feat: fixed metadata to be single origin and return in all scopes

equally and some ui improvement
This commit is contained in:
=
2025-11-13 12:52:39 +05:30
parent a9c4a682d1
commit 4bb13d592f
11 changed files with 165 additions and 116 deletions
@@ -141,7 +141,15 @@ export const registerIdentityProjectMembershipRouter = async (server: FastifyZod
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string()),
metadata: z
.object({
id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()
.optional()
}) })
}) })
}) })
@@ -13,9 +13,7 @@ export const identityV2DALFactory = (db: TDbClient) => {
const doc = await db const doc = await db
.replicaNode()(TableName.Identity) .replicaNode()(TableName.Identity)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`);
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Identity}.orgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.leftJoin( .leftJoin(
TableName.IdentityUniversalAuth, TableName.IdentityUniversalAuth,
@@ -134,9 +132,7 @@ export const identityV2DALFactory = (db: TDbClient) => {
const query = db const query = db
.replicaNode()(TableName.Identity) .replicaNode()(TableName.Identity)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`);
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Identity}.orgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.where(`${TableName.Identity}.orgId`, scopeData.orgId) .where(`${TableName.Identity}.orgId`, scopeData.orgId)
.where((qb) => { .where((qb) => {
@@ -189,9 +189,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
) )
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`paginatedIdentity.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`);
.on(`paginatedIdentity.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`paginatedIdentity.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.leftJoin<TIdentityUniversalAuths>( .leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth, TableName.IdentityUniversalAuth,
@@ -449,9 +447,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`)
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`);
.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.leftJoin( .leftJoin(
TableName.IdentityUniversalAuth, TableName.IdentityUniversalAuth,
@@ -222,8 +222,6 @@ export const identityServiceFactory = ({
const identityDetails = await identityDAL.findById(id); const identityDetails = await identityDAL.findById(id);
console.log("has project id", identityDetails);
if (identityDetails.projectId) { if (identityDetails.projectId) {
throw new BadRequestError({ message: `Identity is managed by project` }); throw new BadRequestError({ message: `Identity is managed by project` });
} }
@@ -40,9 +40,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`);
.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
.where(`${TableName.Membership}.actorIdentityId`, identityId) .where(`${TableName.Membership}.actorIdentityId`, identityId)
@@ -216,7 +214,10 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
] ]
}); });
return data?.[0]; const el = data?.[0];
if (!el) return el;
return { ...el, identity: { ...el.identity, metadata: el.metadata } };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "MembershipGetByIdentityId" }); throw new DatabaseError({ error, name: "MembershipGetByIdentityId" });
} }
@@ -99,3 +99,18 @@ export const useGetProjectIdentityMembership = (projectId: string, identityId: s
} }
}); });
}; };
export const useGetProjectIdentityMembershipV2 = (projectId: string, identityId: string) => {
return useQuery({
enabled: Boolean(projectId && identityId),
queryKey: projectKeys.getProjectIdentityMembershipDetailsV2(projectId, identityId),
queryFn: async () => {
const {
data: { identityMembership }
} = await apiRequest.get<{ identityMembership: IdentityProjectMembership }>(
`/api/v2/projects/${projectId}/identity-memberships/${identityId}`
);
return identityMembership;
}
});
};
@@ -23,6 +23,8 @@ export const projectKeys = {
[{ projectId }, "project-identity-memberships"] as const, [{ projectId }, "project-identity-memberships"] as const,
getProjectIdentityMembershipDetails: (projectId: string, identityId: string) => getProjectIdentityMembershipDetails: (projectId: string, identityId: string) =>
[{ projectId, identityId }, "project-identity-membership-details"] as const, [{ projectId, identityId }, "project-identity-membership-details"] as const,
getProjectIdentityMembershipDetailsV2: (projectId: string, identityId: string) =>
[{ projectId, identityId }, "project-identity-membership-details"] as const,
// allows invalidation using above key without knowing params // allows invalidation using above key without knowing params
getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) => getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) =>
[...projectKeys.getProjectIdentityMemberships(projectId), params] as const, [...projectKeys.getProjectIdentityMemberships(projectId), params] as const,
@@ -242,6 +242,8 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
)} )}
{isOrgIdentity && (
<>
<div> <div>
<FormLabel label="Metadata" /> <FormLabel label="Metadata" />
</div> </div>
@@ -266,7 +268,11 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</div> </div>
<div className="grow"> <div className="grow">
{i === 0 && ( {i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional /> <FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)} )}
<Controller <Controller
control={control} control={control}
@@ -303,6 +309,8 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</Button> </Button>
</div> </div>
</div> </div>
</>
)}
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -28,7 +28,8 @@ import { usePopUp } from "@app/hooks";
import { import {
useAssumeProjectPrivileges, useAssumeProjectPrivileges,
useDeleteProjectIdentityMembership, useDeleteProjectIdentityMembership,
useGetProjectIdentityMembership useGetProjectIdentityMembership,
useGetProjectIdentityMembershipV2
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { projectIdentityQuery } from "@app/hooks/api/projectIdentity"; import { projectIdentityQuery } from "@app/hooks/api/projectIdentity";
@@ -48,7 +49,7 @@ const Page = () => {
const { currentProject, projectId } = useProject(); const { currentProject, projectId } = useProject();
const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } =
useGetProjectIdentityMembership(projectId, identityId); useGetProjectIdentityMembershipV2(projectId, identityId);
const { mutateAsync: deleteMutateAsync, isPending: isDeletingIdentity } = const { mutateAsync: deleteMutateAsync, isPending: isDeletingIdentity } =
useDeleteProjectIdentityMembership(); useDeleteProjectIdentityMembership();
@@ -204,7 +205,7 @@ const Page = () => {
</div> </div>
</PageHeader> </PageHeader>
<div className="flex gap-x-4"> <div className="flex gap-x-4">
{identity && ( {identity ? (
<div className="flex w-72 flex-col gap-y-4"> <div className="flex w-72 flex-col gap-y-4">
<ProjectIdentityDetailsSection <ProjectIdentityDetailsSection
identity={identity} identity={identity}
@@ -215,6 +216,16 @@ const Page = () => {
refetchIdentity={() => refetchIdentity()} refetchIdentity={() => refetchIdentity()}
/> />
</div> </div>
) : (
<div>
<div className="flex w-72 flex-col gap-y-4">
<ProjectIdentityDetailsSection
identity={{ ...identityMembershipDetails?.identity, projectId: "" }}
isOrgIdentity
membership={identityMembershipDetails!}
/>
</div>
</div>
)} )}
<div className="flex-1"> <div className="flex-1">
<IdentityRoleDetailsSection <IdentityRoleDetailsSection
@@ -26,6 +26,7 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionSub, ProjectPermissionSub,
useProject,
useProjectPermission useProjectPermission
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
@@ -46,13 +47,13 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
] as const); ] as const);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const identityId = identityMembershipDetails?.identity?.id; const identityId = identityMembershipDetails?.identity?.id;
const projectId = identityMembershipDetails?.project?.id; const { projectId } = useProject();
const { mutateAsync: deletePrivilege } = useDeleteIdentityProjectAdditionalPrivilege(); const { mutateAsync: deletePrivilege } = useDeleteIdentityProjectAdditionalPrivilege();
const { data: identityProjectPrivileges, isPending } = useListIdentityProjectPrivileges({ const { data: identityProjectPrivileges, isPending } = useListIdentityProjectPrivileges({
identityId: identityMembershipDetails?.identity?.id, identityId: identityMembershipDetails?.identity?.id,
projectId: identityMembershipDetails?.project?.id projectId
}); });
const handlePrivilegeDelete = async () => { const handlePrivilegeDelete = async () => {
@@ -34,10 +34,11 @@ import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/compo
type Props = { type Props = {
identity: TProjectIdentity; identity: TProjectIdentity;
isOrgIdentity?: boolean;
membership: IdentityProjectMembership; membership: IdentityProjectMembership;
}; };
export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) => { export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, membership }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({ const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard" initialState: "Copy ID to clipboard"
}); });
@@ -76,6 +77,7 @@ export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) =
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Identity Details</h3> <h3 className="text-lg font-medium text-mineshaft-100">Identity Details</h3>
<DropdownMenu> <DropdownMenu>
{!isOrgIdentity && (
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
size="xs" size="xs"
@@ -91,6 +93,7 @@ export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) =
Options Options
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
)}
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end"> <DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionIdentityActions.Edit} I={ProjectPermissionIdentityActions.Edit}
@@ -161,6 +164,14 @@ export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) =
</div> </div>
</div> </div>
</div> </div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Managed By</p>
<p className="text-sm text-mineshaft-300">
{identity.projectId ? "Project" : "Organization"}
</p>
</div>
{!isOrgIdentity && (
<>
<div className="mb-4"> <div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p> <p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
<p className="text-sm text-mineshaft-300"> <p className="text-sm text-mineshaft-300">
@@ -181,6 +192,8 @@ export const ProjectIdentityDetailsSection = ({ identity, membership }: Props) =
{identity.hasDeleteProtection ? "On" : "Off"} {identity.hasDeleteProtection ? "On" : "Off"}
</p> </p>
</div> </div>
</>
)}
<div> <div>
<p className="text-sm font-medium text-mineshaft-300">Metadata</p> <p className="text-sm font-medium text-mineshaft-300">Metadata</p>
{identity?.metadata?.length ? ( {identity?.metadata?.length ? (