mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 07:27:43 +00:00
feat(rbac): implemented project based permission loading and role management
This commit is contained in:
@@ -14,9 +14,15 @@ import {
|
|||||||
DeleteRoleSchema,
|
DeleteRoleSchema,
|
||||||
GetRoleSchema,
|
GetRoleSchema,
|
||||||
GetUserPermission,
|
GetUserPermission,
|
||||||
|
GetUserProjectPermission,
|
||||||
UpdateRoleSchema
|
UpdateRoleSchema
|
||||||
} from "../../validation";
|
} from "../../validation";
|
||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
import {
|
||||||
|
adminProjectPermissions,
|
||||||
|
getUserProjectPermissions,
|
||||||
|
viewerProjectPermission
|
||||||
|
} from "../../services/ProjectRoleService";
|
||||||
|
|
||||||
export const createRole = async (req: Request, res: Response) => {
|
export const createRole = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
@@ -130,35 +136,45 @@ export const getRoles = async (req: Request, res: Response) => {
|
|||||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
||||||
|
const roles = [
|
||||||
|
{
|
||||||
|
_id: "admin",
|
||||||
|
name: "Admin",
|
||||||
|
slug: "admin",
|
||||||
|
description: "Complete administration access over the organization",
|
||||||
|
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "member",
|
||||||
|
name: "Member",
|
||||||
|
slug: "member",
|
||||||
|
description: "Non-administrative role in an organization",
|
||||||
|
permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "viewer",
|
||||||
|
name: "Viewer",
|
||||||
|
slug: "viewer",
|
||||||
|
description: "Non-administrative role in an organization",
|
||||||
|
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
|
||||||
|
},
|
||||||
|
...customRoles
|
||||||
|
];
|
||||||
|
if (isOrgRole) {
|
||||||
|
roles.unshift({
|
||||||
|
_id: "owner",
|
||||||
|
name: "Owner",
|
||||||
|
slug: "owner",
|
||||||
|
description: "Complete administration access over the organization.",
|
||||||
|
permissions: adminPermissions.rules
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: "Successfully fetched role list",
|
message: "Successfully fetched role list",
|
||||||
data: {
|
data: {
|
||||||
roles: [
|
roles
|
||||||
{
|
|
||||||
_id: "owner",
|
|
||||||
name: "Owner",
|
|
||||||
slug: "owner",
|
|
||||||
description: "Complete administration access over the organization.",
|
|
||||||
permissions: adminPermissions.rules
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "admin",
|
|
||||||
name: "Admin",
|
|
||||||
slug: "admin",
|
|
||||||
description: "Complete administration access over the organization",
|
|
||||||
permissions: adminPermissions.rules
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "member",
|
|
||||||
name: "Member",
|
|
||||||
slug: "member",
|
|
||||||
description: "Non-administrative role in an organization",
|
|
||||||
permissions: memberPermissions.rules
|
|
||||||
},
|
|
||||||
...roles
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -175,3 +191,16 @@ export const getUserPermissions = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
params: { workspaceId }
|
||||||
|
} = await validateRequest(GetUserProjectPermission, req);
|
||||||
|
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
data: {
|
||||||
|
permissions: packRules(permission.rules)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const validateMembership = async ({
|
|||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId | string;
|
userId: Types.ObjectId | string;
|
||||||
workspaceId: Types.ObjectId | string;
|
workspaceId: Types.ObjectId | string;
|
||||||
acceptedRoles?: Array<"admin" | "member" | "custom">;
|
acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">;
|
||||||
}) => {
|
}) => {
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
user: userId,
|
user: userId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, Types, model } from "mongoose";
|
||||||
import { ADMIN, CUSTOM, MEMBER } from "../variables";
|
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
|
||||||
|
|
||||||
export interface IMembershipPermission {
|
export interface IMembershipPermission {
|
||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
@@ -11,7 +11,7 @@ export interface IMembership {
|
|||||||
user: Types.ObjectId;
|
user: Types.ObjectId;
|
||||||
inviteEmail?: string;
|
inviteEmail?: string;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
role: "admin" | "member" | "custom";
|
role: "admin" | "member" | "viewer" | "custom";
|
||||||
customRole: Types.ObjectId;
|
customRole: Types.ObjectId;
|
||||||
deniedPermissions: IMembershipPermission[];
|
deniedPermissions: IMembershipPermission[];
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ const membershipSchema = new Schema<IMembership>(
|
|||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ADMIN, MEMBER, CUSTOM],
|
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
customRole: {
|
customRole: {
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleControll
|
|||||||
|
|
||||||
// get a user permissions in an org
|
// get a user permissions in an org
|
||||||
router.get(
|
router.get(
|
||||||
"/:orgId/permissions",
|
"/organization/:orgId/permissions",
|
||||||
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
|
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
|
||||||
roleController.getUserPermissions
|
roleController.getUserPermissions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/workspace/:workspaceId/permissions",
|
||||||
|
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
|
||||||
|
roleController.getUserWorkspacePermissions
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
|
||||||
|
import { Membership } from "../models";
|
||||||
|
import { IRole } from "../models/role";
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
|
|
||||||
|
export enum GeneralPermissionActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermission {
|
||||||
|
Role = "role",
|
||||||
|
Member = "member",
|
||||||
|
Settings = "settings",
|
||||||
|
Integrations = "integrations",
|
||||||
|
Webhooks = "webhooks",
|
||||||
|
ServiceTokens = "service-tokens",
|
||||||
|
Environments = "environments",
|
||||||
|
Tags = "tags",
|
||||||
|
AuditLogs = "audit-logs",
|
||||||
|
IpAllowList = "ip-allowlist",
|
||||||
|
Workspace = "workspace",
|
||||||
|
Secrets = "secrets",
|
||||||
|
SecretImports = "secret-imports",
|
||||||
|
Folders = "folders"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectPermissionSet =
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Secrets]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Folders]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.SecretImports]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Role]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Tags]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Member]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Integrations]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Webhooks]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.AuditLogs]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Environments]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.IpAllowList]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.Settings]
|
||||||
|
| [GeneralPermissionActions, ProjectPermission.ServiceTokens]
|
||||||
|
| [GeneralPermissionActions.Delete, ProjectPermission.Workspace]
|
||||||
|
| [GeneralPermissionActions.Edit, ProjectPermission.Workspace];
|
||||||
|
|
||||||
|
const buildAdminPermission = () => {
|
||||||
|
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Secrets);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Folders);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Member);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Member);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Member);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Member);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Role);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Role);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Role);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Role);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Integrations);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Integrations);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Integrations);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Webhooks);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Webhooks);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Webhooks);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.ServiceTokens);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.ServiceTokens);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.ServiceTokens);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Settings);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Settings);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Settings);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Environments);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Environments);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Environments);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Tags);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Tags);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Tags);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.AuditLogs);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.AuditLogs);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.AuditLogs);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.IpAllowList);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.IpAllowList);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Workspace);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList);
|
||||||
|
|
||||||
|
return build();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adminProjectPermissions = buildAdminPermission();
|
||||||
|
|
||||||
|
const buildMemberPermission = () => {
|
||||||
|
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Secrets);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.Folders);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Create, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Member);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Role);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
|
||||||
|
|
||||||
|
return build();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const memberProjectPermissions = buildMemberPermission();
|
||||||
|
|
||||||
|
const buildViewerPermission = () => {
|
||||||
|
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Member);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Role);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
|
||||||
|
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
|
||||||
|
|
||||||
|
return build();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewerProjectPermission = buildViewerPermission();
|
||||||
|
|
||||||
|
export const getUserProjectPermissions = async (userId: string, workspaceId: string) => {
|
||||||
|
// TODO(akhilmhdh): speed this up by pulling from cache later
|
||||||
|
const membership = await Membership.findOne({
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
|
||||||
|
}>("customRole")
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
console.log(membership, userId, workspaceId);
|
||||||
|
if (!membership || (membership.role === "custom" && !membership.customRole)) {
|
||||||
|
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membership.role === "admin") return { permission: adminProjectPermissions, membership };
|
||||||
|
if (membership.role === "member") return { permission: memberProjectPermissions, membership };
|
||||||
|
if (membership.role === "viewer") return { permission: memberProjectPermissions, membership };
|
||||||
|
|
||||||
|
if (membership.role === "custom") {
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions);
|
||||||
|
return { permission, membership };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw BadRequestError({ message: "User role not found" });
|
||||||
|
};
|
||||||
@@ -56,3 +56,9 @@ export const GetUserPermission = z.object({
|
|||||||
orgId: z.string().trim()
|
orgId: z.string().trim()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const GetUserProjectPermission = z.object({
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export const OWNER = "owner";
|
export const OWNER = "owner";
|
||||||
export const ADMIN = "admin";
|
export const ADMIN = "admin";
|
||||||
export const MEMBER = "member";
|
export const MEMBER = "member";
|
||||||
|
export const VIEWER = "viewer";
|
||||||
export const CUSTOM = "custom";
|
export const CUSTOM = "custom";
|
||||||
|
|
||||||
// membership statuses
|
// membership statuses
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { FunctionComponent, ReactNode } from "react";
|
import { FunctionComponent, ReactNode } from "react";
|
||||||
import { BoundCanProps, Can } from "@casl/react";
|
import { BoundCanProps, Can } from "@casl/react";
|
||||||
|
|
||||||
import {
|
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
|
||||||
OrgPermissionSubjects,
|
|
||||||
OrgWorkspacePermissionActions,
|
|
||||||
TOrgPermission,
|
|
||||||
useOrgPermission
|
|
||||||
} from "@app/context/OrgPermissionContext";
|
|
||||||
|
|
||||||
import { Tooltip } from "../v2";
|
import { Tooltip } from "../v2";
|
||||||
|
|
||||||
@@ -23,13 +18,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
|||||||
const permission = useOrgPermission();
|
const permission = useOrgPermission();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Can
|
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
|
||||||
{...props}
|
|
||||||
passThrough={passThrough}
|
|
||||||
ability={props?.ability || permission}
|
|
||||||
I={OrgWorkspacePermissionActions.Read}
|
|
||||||
a={OrgPermissionSubjects.Sso}
|
|
||||||
>
|
|
||||||
{(isAllowed, ability) => {
|
{(isAllowed, ability) => {
|
||||||
// akhilmhdh: This is set as type due to error in casl react type.
|
// akhilmhdh: This is set as type due to error in casl react type.
|
||||||
const finalChild =
|
const finalChild =
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { FunctionComponent, ReactNode } from "react";
|
||||||
|
import { BoundCanProps, Can } from "@casl/react";
|
||||||
|
|
||||||
|
import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext";
|
||||||
|
|
||||||
|
import { Tooltip } from "../v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label?: ReactNode;
|
||||||
|
} & BoundCanProps<TProjectPermission>;
|
||||||
|
|
||||||
|
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||||
|
label = "Permission Denied. Kindly contact your org admin",
|
||||||
|
children,
|
||||||
|
passThrough = true,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const permission = useProjectPermission();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
|
||||||
|
{(isAllowed, ability) => {
|
||||||
|
// akhilmhdh: This is set as type due to error in casl react type.
|
||||||
|
const finalChild =
|
||||||
|
typeof children === "function"
|
||||||
|
? children(isAllowed, ability as TProjectPermission)
|
||||||
|
: children;
|
||||||
|
|
||||||
|
if (!isAllowed && passThrough) {
|
||||||
|
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllowed) return null;
|
||||||
|
|
||||||
|
return finalChild;
|
||||||
|
}}
|
||||||
|
</Can>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { OrgPermissionCan } from "./OrgPermissionCan";
|
export { OrgPermissionCan } from "./OrgPermissionCan";
|
||||||
|
export { ProjectPermissionCan } from "./ProjectPermissionCan";
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
|
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
|
||||||
export type { TOrgPermission } from "./types";
|
export type { TOrgPermission } from "./types";
|
||||||
export {
|
export { GeneralPermissionActions,OrgPermissionSubjects } from "./types";
|
||||||
OrgGeneralPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
OrgWorkspacePermissionActions
|
|
||||||
} from "./types";
|
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { MongoAbility } from "@casl/ability";
|
import { MongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
export enum OrgGeneralPermissionActions {
|
export enum GeneralPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrgWorkspacePermissionActions {
|
|
||||||
Read = "read",
|
|
||||||
Create = "create"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@@ -24,13 +19,14 @@ export enum OrgPermissionSubjects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
| [OrgWorkspacePermissionActions, OrgPermissionSubjects.Workspace]
|
| [GeneralPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Role]
|
| [GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Member]
|
| [GeneralPermissionActions, OrgPermissionSubjects.Role]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Settings]
|
| [GeneralPermissionActions, OrgPermissionSubjects.Member]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
| [GeneralPermissionActions, OrgPermissionSubjects.Settings]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Sso]
|
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [GeneralPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Billing];
|
| [GeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
|
| [GeneralPermissionActions, OrgPermissionSubjects.Billing];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { createContext, ReactNode, useContext } from "react";
|
||||||
|
|
||||||
|
import { useGetUserProjectPermissions } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { useWorkspace } from "../WorkspaceContext";
|
||||||
|
import { TProjectPermission } from "./types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectPermissionContext = createContext<null | TProjectPermission>(null);
|
||||||
|
|
||||||
|
export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
|
||||||
|
|
||||||
|
if (isLoading && workspaceId) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permission && currentWorkspace) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||||
|
Failed to load user permissions
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
return <>children</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectPermissionContext.Provider value={permission}>
|
||||||
|
{children}
|
||||||
|
</ProjectPermissionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectPermission = () => {
|
||||||
|
const ctx = useContext(ProjectPermissionContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
|
||||||
|
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||||
|
export { ProjectGeneralPermissionActions, ProjectPermissionSubjects } from "./types";
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { MongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
|
export enum ProjectGeneralPermissionActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSubjects {
|
||||||
|
Role = "role",
|
||||||
|
Member = "member",
|
||||||
|
Settings = "settings",
|
||||||
|
Integrations = "integrations",
|
||||||
|
Webhooks = "webhooks",
|
||||||
|
ServiceTokens = "service-tokens",
|
||||||
|
Environments = "environments",
|
||||||
|
Tags = "tags",
|
||||||
|
AuditLogs = "audit-logs",
|
||||||
|
IpAllowList = "ip-allowlist",
|
||||||
|
Workspace = "workspace",
|
||||||
|
Secrets = "secrets",
|
||||||
|
SecretImports = "secret-imports",
|
||||||
|
Folders = "folders"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectPermissionSet =
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Secrets]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Folders]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.SecretImports]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Role]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Tags]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Member]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Integrations]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Webhooks]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.AuditLogs]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Environments]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.IpAllowList]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Settings]
|
||||||
|
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.ServiceTokens]
|
||||||
|
| [ProjectGeneralPermissionActions.Delete, ProjectPermissionSubjects.Workspace]
|
||||||
|
| [ProjectGeneralPermissionActions.Edit, ProjectPermissionSubjects.Workspace];
|
||||||
|
|
||||||
|
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||||
@@ -2,11 +2,17 @@ export { AuthProvider } from "./AuthContext";
|
|||||||
export { OrgProvider, useOrganization } from "./OrganizationContext";
|
export { OrgProvider, useOrganization } from "./OrganizationContext";
|
||||||
export type { TOrgPermission } from "./OrgPermissionContext";
|
export type { TOrgPermission } from "./OrgPermissionContext";
|
||||||
export {
|
export {
|
||||||
OrgGeneralPermissionActions,
|
GeneralPermissionActions,
|
||||||
|
OrgPermissionProvider,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
OrgWorkspacePermissionActions
|
useOrgPermission
|
||||||
} from "./OrgPermissionContext";
|
} from "./OrgPermissionContext";
|
||||||
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
|
export {
|
||||||
|
ProjectGeneralPermissionActions,
|
||||||
|
ProjectPermissionProvider,
|
||||||
|
ProjectPermissionSubjects,
|
||||||
|
useProjectPermission
|
||||||
|
} from "./ProjectPermissionContext";
|
||||||
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
||||||
export { UserProvider, useUser } from "./UserContext";
|
export { UserProvider, useUser } from "./UserContext";
|
||||||
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { useCreateRole, useDeleteRole, useUpdateRole } from "./mutation";
|
export { useCreateRole, useDeleteRole, useUpdateRole } from "./mutation";
|
||||||
export { useGetRoles, useGetUserOrgPermissions } from "./queries";
|
export { useGetRoles, useGetUserOrgPermissions,useGetUserProjectPermissions } from "./queries";
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { roleQueryKeys } from "./queries";
|
import { roleQueryKeys } from "./queries";
|
||||||
import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types";
|
import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types";
|
||||||
|
|
||||||
export const useCreateRole = () => {
|
export const useCreateRole = <T extends string | undefined>() => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (dto: TCreateRoleDTO) => apiRequest.post("/api/v1/roles", dto),
|
mutationFn: (dto: TCreateRoleDTO<T>) => apiRequest.post("/api/v1/roles", dto),
|
||||||
onSuccess: (_, { orgId, workspaceId }) => {
|
onSuccess: (_, { orgId, workspaceId }) => {
|
||||||
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateRole = () => {
|
export const useUpdateRole = <T extends string | undefined>() => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, ...dto }: TUpdateRoleDTO) => apiRequest.patch(`/api/v1/roles/${id}`, dto),
|
mutationFn: ({ id, ...dto }: TUpdateRoleDTO<T>) => apiRequest.patch(`/api/v1/roles/${id}`, dto),
|
||||||
onSuccess: (_, { orgId, workspaceId }) => {
|
onSuccess: (_, { orgId, workspaceId }) => {
|
||||||
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,33 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
|
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
import { TGetRolesDTO, TGetUserOrgPermissionsDTO, TRole } from "./types";
|
import {
|
||||||
|
TGetRolesDTO,
|
||||||
|
TGetUserOrgPermissionsDTO,
|
||||||
|
TGetUserProjectPermissionDTO,
|
||||||
|
TRole
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const roleQueryKeys = {
|
export const roleQueryKeys = {
|
||||||
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
|
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
|
||||||
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
||||||
["user-permissions", { orgId }] as const
|
["user-permissions", { orgId }] as const,
|
||||||
|
getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) =>
|
||||||
|
["user-project-permissions", { workspaceId }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRoles = async ({ orgId, workspaceId }: TGetRolesDTO) => {
|
const getRoles = async ({ orgId, workspaceId }: TGetRolesDTO) => {
|
||||||
const { data } = await apiRequest.get<{ data: { roles: TRole[] } }>("/api/v1/roles", {
|
const { data } = await apiRequest.get<{ data: { roles: TRole<typeof workspaceId>[] } }>(
|
||||||
params: {
|
"/api/v1/roles",
|
||||||
workspaceId,
|
{
|
||||||
orgId
|
params: {
|
||||||
|
workspaceId,
|
||||||
|
orgId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return data.data.roles;
|
return data.data.roles;
|
||||||
};
|
};
|
||||||
@@ -34,7 +45,7 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
|
|||||||
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
|
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
|
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
|
||||||
}>(`/api/v1/roles/${orgId}/permissions`, {});
|
}>(`/api/v1/roles/organization/${orgId}/permissions`, {});
|
||||||
|
|
||||||
return data.data.permissions;
|
return data.data.permissions;
|
||||||
};
|
};
|
||||||
@@ -50,3 +61,23 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
|
|||||||
return ability;
|
return ability;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermissionDTO) => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
|
||||||
|
}>(`/api/v1/roles/workspace/${workspaceId}/permissions`, {});
|
||||||
|
|
||||||
|
return data.data.permissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }),
|
||||||
|
queryFn: () => getUserProjectPermissions({ workspaceId }),
|
||||||
|
enabled: Boolean(workspaceId),
|
||||||
|
select: (data) => {
|
||||||
|
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data);
|
||||||
|
const ability = createMongoAbility<ProjectPermissionSet>(rule);
|
||||||
|
return ability;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ export type TGetRolesDTO = {
|
|||||||
workspaceId?: string;
|
workspaceId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TRole = {
|
export type TRole<T extends string | undefined> = {
|
||||||
_id: string;
|
_id: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
workspace: string;
|
workspace: T;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
permissions: TPermission[];
|
permissions: T extends string ? TProjectPermission[] : TPermission[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
@@ -29,20 +29,42 @@ type TWorkspacePermission = {
|
|||||||
subject: "workspace";
|
subject: "workspace";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateRoleDTO = {
|
export type TProjectPermission = TProjectGeneralPermission | TProjectWorkspacePermission;
|
||||||
|
|
||||||
|
type TProjectGeneralPermission = {
|
||||||
|
condition?: Record<string, any>;
|
||||||
|
action: "read" | "edit" | "create" | "delete";
|
||||||
|
subject:
|
||||||
|
| "member"
|
||||||
|
| "role"
|
||||||
|
| "settings"
|
||||||
|
| "secrets"
|
||||||
|
| "environments"
|
||||||
|
| "folders"
|
||||||
|
| "secret-imports"
|
||||||
|
| "service-tokens";
|
||||||
|
};
|
||||||
|
|
||||||
|
type TProjectWorkspacePermission = {
|
||||||
|
condition?: Record<string, any>;
|
||||||
|
action: "delete" | "edit";
|
||||||
|
subject: "workspace";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateRoleDTO<T extends string | undefined> = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
workspaceId?: string;
|
workspaceId?: T;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
permissions: TPermission[];
|
permissions: T extends string ? TProjectPermission[] : TPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateRoleDTO = {
|
export type TUpdateRoleDTO<T extends string | undefined> = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId?: string;
|
workspaceId?: T;
|
||||||
} & Partial<Omit<TCreateRoleDTO, "orgId" | "workspaceId">>;
|
} & Partial<Omit<TCreateRoleDTO<T>, "orgId" | "workspaceId">>;
|
||||||
|
|
||||||
export type TDeleteRoleDTO = {
|
export type TDeleteRoleDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -53,3 +75,7 @@ export type TDeleteRoleDTO = {
|
|||||||
export type TGetUserOrgPermissionsDTO = {
|
export type TGetUserOrgPermissionsDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetUserProjectPermissionDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { useUploadWsKey } from "../keys/queries";
|
import { useUploadWsKey } from "../keys/queries";
|
||||||
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import {
|
import {
|
||||||
AddUserToOrgDTO,
|
AddUserToOrgDTO,
|
||||||
AddUserToWsDTO,
|
AddUserToWsDTO,
|
||||||
@@ -55,24 +56,24 @@ export const useRenameUser = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, RenameUserDTO>({
|
return useMutation<{}, {}, RenameUserDTO>({
|
||||||
mutationFn: ({ newName }) =>
|
mutationFn: ({ newName }) =>
|
||||||
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
|
apiRequest.patch("/api/v2/users/me/name", {
|
||||||
|
firstName: newName?.split(" ")[0],
|
||||||
|
lastName: newName?.split(" ").slice(1).join(" ")
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(userKeys.getUser);
|
queryClient.invalidateQueries(userKeys.getUser);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useUpdateUserAuthMethods = () => {
|
export const useUpdateUserAuthMethods = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ authMethods }: { authMethods: AuthMethod[] }) => {
|
||||||
authMethods
|
const {
|
||||||
}: {
|
data: { user }
|
||||||
authMethods: AuthMethod[];
|
} = await apiRequest.put("/api/v2/users/me/auth-methods", {
|
||||||
}) => {
|
|
||||||
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
|
|
||||||
authMethods
|
authMethods
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ export const useGetOrgUsers = (orgId: string) =>
|
|||||||
// mutation
|
// mutation
|
||||||
export const useAddUserToWs = () => {
|
export const useAddUserToWs = () => {
|
||||||
const uploadWsKey = useUploadWsKey();
|
const uploadWsKey = useUploadWsKey();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
|
return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
|
||||||
mutationFn: ({ email, workspaceId }) =>
|
mutationFn: ({ email, workspaceId }) =>
|
||||||
@@ -136,18 +138,20 @@ export const useAddUserToWs = () => {
|
|||||||
userId: data.invitee._id,
|
userId: data.invitee._id,
|
||||||
workspaceId
|
workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAddUserToOrg = () => {
|
export const useAddUserToOrg = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
type Response = {
|
type Response = {
|
||||||
data: {
|
data: {
|
||||||
message: string,
|
message: string;
|
||||||
completeInviteLink: string | undefined
|
completeInviteLink: string | undefined;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
return useMutation<Response, {}, AddUserToOrgDTO>({
|
return useMutation<Response, {}, AddUserToOrgDTO>({
|
||||||
mutationFn: (dto) => {
|
mutationFn: (dto) => {
|
||||||
@@ -164,7 +168,7 @@ export const useDeleteOrgMembership = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, DeletOrgMembershipDTO>({
|
return useMutation<{}, {}, DeletOrgMembershipDTO>({
|
||||||
mutationFn: ({ membershipId, orgId }) => {
|
mutationFn: ({ membershipId, orgId }) => {
|
||||||
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`)
|
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
|
||||||
},
|
},
|
||||||
onSuccess: (_, { orgId }) => {
|
onSuccess: (_, { orgId }) => {
|
||||||
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
|
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
|
||||||
@@ -177,9 +181,12 @@ export const useUpdateOrgUserRole = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
|
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
|
||||||
mutationFn: ({ organizationId, membershipId, role }) => {
|
mutationFn: ({ organizationId, membershipId, role }) => {
|
||||||
return apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
return apiRequest.patch(
|
||||||
role
|
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
|
||||||
});
|
{
|
||||||
|
role
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: (_, { organizationId }) => {
|
onSuccess: (_, { organizationId }) => {
|
||||||
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
@@ -218,48 +225,35 @@ export const useLogoutUser = () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const useGetMyIp = () => {
|
export const useGetMyIp = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: userKeys.myIp,
|
queryKey: userKeys.myIp,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ ip: string; }>(
|
const { data } = await apiRequest.get<{ ip: string }>("/api/v1/users/me/ip");
|
||||||
"/api/v1/users/me/ip"
|
|
||||||
);
|
|
||||||
return data.ip;
|
return data.ip;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetMyAPIKeys = () => {
|
export const useGetMyAPIKeys = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: userKeys.myAPIKeys,
|
queryKey: userKeys.myAPIKeys,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<APIKeyData[]>(
|
const { data } = await apiRequest.get<APIKeyData[]>("/api/v2/users/me/api-keys");
|
||||||
"/api/v2/users/me/api-keys"
|
|
||||||
);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useCreateAPIKey = () => {
|
export const useCreateAPIKey = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => {
|
||||||
name,
|
const { data } = await apiRequest.post<CreateAPIKeyRes>("/api/v2/users/me/api-keys", {
|
||||||
expiresIn
|
name,
|
||||||
}: {
|
expiresIn
|
||||||
name: string;
|
});
|
||||||
expiresIn: number;
|
|
||||||
}) => {
|
|
||||||
const { data } = await apiRequest.post<CreateAPIKeyRes>(
|
|
||||||
"/api/v2/users/me/api-keys",
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
expiresIn
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -267,15 +261,13 @@ export const useCreateAPIKey = () => {
|
|||||||
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useDeleteAPIKey = () => {
|
export const useDeleteAPIKey = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (apiKeyDataId: string) => {
|
mutationFn: async (apiKeyDataId: string) => {
|
||||||
const { data } = await apiRequest.delete(
|
const { data } = await apiRequest.delete(`/api/v2/users/me/api-keys/${apiKeyDataId}`);
|
||||||
`/api/v2/users/me/api-keys/${apiKeyDataId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -283,29 +275,25 @@ export const useDeleteAPIKey = () => {
|
|||||||
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetMySessions = () => {
|
export const useGetMySessions = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: userKeys.mySessions,
|
queryKey: userKeys.mySessions,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<TokenVersion[]>(
|
const { data } = await apiRequest.get<TokenVersion[]>("/api/v2/users/me/sessions");
|
||||||
"/api/v2/users/me/sessions"
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useRevokeMySessions = () => {
|
export const useRevokeMySessions = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const { data } = await apiRequest.delete(
|
const { data } = await apiRequest.delete("/api/v2/users/me/sessions");
|
||||||
"/api/v2/users/me/sessions"
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -313,22 +301,17 @@ export const useRevokeMySessions = () => {
|
|||||||
queryClient.invalidateQueries(userKeys.mySessions);
|
queryClient.invalidateQueries(userKeys.mySessions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useUpdateMfaEnabled = () => {
|
export const useUpdateMfaEnabled = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => {
|
||||||
isMfaEnabled
|
const {
|
||||||
}: {
|
data: { user }
|
||||||
isMfaEnabled: boolean;
|
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
||||||
}) => {
|
isMfaEnabled
|
||||||
const { data: { user } } = await apiRequest.patch(
|
});
|
||||||
"/api/v2/users/me/mfa",
|
|
||||||
{
|
|
||||||
isMfaEnabled
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
@@ -336,15 +319,15 @@ export const useUpdateMfaEnabled = () => {
|
|||||||
queryClient.invalidateQueries(userKeys.getUser);
|
queryClient.invalidateQueries(userKeys.getUser);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const fetchMyOrganizationProjects = async (orgId: string) => {
|
export const fetchMyOrganizationProjects = async (orgId: string) => {
|
||||||
const { data: { workspaces } } = await apiRequest.get(
|
const {
|
||||||
`/api/v1/organization/${orgId}/my-workspaces`
|
data: { workspaces }
|
||||||
);
|
} = await apiRequest.get(`/api/v1/organization/${orgId}/my-workspaces`);
|
||||||
|
|
||||||
return workspaces;
|
return workspaces;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetMyOrganizationProjects = (orgId: string) => {
|
export const useGetMyOrganizationProjects = (orgId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -354,4 +337,4 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
|
|||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
|
|
||||||
export enum AuthMethod {
|
export enum AuthMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
GITHUB = "github",
|
GITHUB = "github",
|
||||||
OKTA_SAML = "okta-saml",
|
OKTA_SAML = "okta-saml",
|
||||||
AZURE_SAML = "azure-saml",
|
AZURE_SAML = "azure-saml",
|
||||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@@ -48,6 +48,8 @@ export type OrgUser = {
|
|||||||
customRole: string;
|
customRole: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TWorkspaceUser = OrgUser;
|
||||||
|
|
||||||
export type AddUserToWsDTO = {
|
export type AddUserToWsDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { IntegrationAuth } from "../integrationAuth/types";
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
import { TIntegration } from "../integrations/types";
|
import { TIntegration } from "../integrations/types";
|
||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
|
import { TWorkspaceUser } from "../users/types";
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
@@ -173,16 +174,17 @@ export const createWorkspace = ({
|
|||||||
workspaceName
|
workspaceName
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
|
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
|
||||||
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
|
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ organizationId, workspaceName }) => createWorkspace({
|
mutationFn: async ({ organizationId, workspaceName }) =>
|
||||||
organizationId,
|
createWorkspace({
|
||||||
workspaceName
|
organizationId,
|
||||||
}),
|
workspaceName
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
}
|
}
|
||||||
@@ -296,32 +298,30 @@ export const useGetWorkspaceUsers = (workspaceId: string) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data: { users } } = await apiRequest.get(
|
const {
|
||||||
|
data: { users }
|
||||||
|
} = await apiRequest.get<{ users: TWorkspaceUser[] }>(
|
||||||
`/api/v1/workspace/${workspaceId}/users`
|
`/api/v1/workspace/${workspaceId}/users`
|
||||||
);
|
);
|
||||||
return users;
|
return users;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useAddUserToWorkspace = () => {
|
export const useAddUserToWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
|
||||||
email,
|
const {
|
||||||
workspaceId
|
data: { invitee, latestKey }
|
||||||
}: {
|
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
|
||||||
email: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}) => {
|
|
||||||
const { data: { invitee, latestKey } } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
|
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
invitee,
|
invitee,
|
||||||
latestKey
|
latestKey
|
||||||
});
|
};
|
||||||
},
|
},
|
||||||
onSuccess: (_, dto) => {
|
onSuccess: (_, dto) => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId));
|
||||||
@@ -334,7 +334,9 @@ export const useDeleteUserFromWorkspace = () => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (membershipId: string) => {
|
mutationFn: async (membershipId: string) => {
|
||||||
const { data: { deletedMembership } } = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
|
const {
|
||||||
|
data: { deletedMembership }
|
||||||
|
} = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
|
||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
},
|
},
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
@@ -346,14 +348,10 @@ export const useDeleteUserFromWorkspace = () => {
|
|||||||
export const useUpdateUserWorkspaceRole = () => {
|
export const useUpdateUserWorkspaceRole = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ membershipId, role }: { membershipId: string; role: string }) => {
|
||||||
membershipId,
|
const {
|
||||||
role
|
data: { membership }
|
||||||
}: {
|
} = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
|
||||||
membershipId: string;
|
|
||||||
role: string;
|
|
||||||
}) => {
|
|
||||||
const { data: { membership } } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
|
|
||||||
role
|
role
|
||||||
});
|
});
|
||||||
return membership;
|
return membership;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
AuthProvider,
|
AuthProvider,
|
||||||
OrgPermissionProvider,
|
OrgPermissionProvider,
|
||||||
OrgProvider,
|
OrgProvider,
|
||||||
|
ProjectPermissionProvider,
|
||||||
SubscriptionProvider,
|
SubscriptionProvider,
|
||||||
UserProvider,
|
UserProvider,
|
||||||
WorkspaceProvider
|
WorkspaceProvider
|
||||||
@@ -98,15 +99,17 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
<OrgProvider>
|
<OrgProvider>
|
||||||
<OrgPermissionProvider>
|
<OrgPermissionProvider>
|
||||||
<WorkspaceProvider>
|
<WorkspaceProvider>
|
||||||
<SubscriptionProvider>
|
<ProjectPermissionProvider>
|
||||||
<UserProvider>
|
<SubscriptionProvider>
|
||||||
<NotificationProvider>
|
<UserProvider>
|
||||||
|
<NotificationProvider>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</SubscriptionProvider>
|
</SubscriptionProvider>
|
||||||
|
</ProjectPermissionProvider>
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
</OrgPermissionProvider>
|
</OrgPermissionProvider>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
|
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ const SettingsBilling = withPermission<{}, TOrgPermission>(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
|
{ action: GeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(SettingsBilling, { requireAuth: true });
|
Object.assign(SettingsBilling, { requireAuth: true });
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import {
|
import {
|
||||||
|
GeneralPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
OrgWorkspacePermissionActions,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -590,7 +590,7 @@ const OrganizationPage = withPermission(
|
|||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgWorkspacePermissionActions.Create}
|
I={GeneralPermissionActions.Create}
|
||||||
an={OrgPermissionSubjects.Workspace}
|
an={OrgPermissionSubjects.Workspace}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -877,7 +877,7 @@ const OrganizationPage = withPermission(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: OrgWorkspacePermissionActions.Read,
|
action: GeneralPermissionActions.Read,
|
||||||
subject: OrgPermissionSubjects.Workspace
|
subject: OrgPermissionSubjects.Workspace
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ const SecretScanning = withPermission(
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center h-[3.25rem]">
|
<div className="flex items-center h-[3.25rem]">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Create}
|
I={GeneralPermissionActions.Create}
|
||||||
a={OrgPermissionSubjects.SecretScanning}
|
a={OrgPermissionSubjects.SecretScanning}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -125,7 +125,7 @@ const SecretScanning = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
|
{ action: GeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(SecretScanning, { requireAuth: true });
|
Object.assign(SecretScanning, { requireAuth: true });
|
||||||
|
|||||||
@@ -1,225 +1,21 @@
|
|||||||
import { useEffect, useState } from "react";
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import Button from "@app/components/basic/buttons/Button";
|
import { MembersPage } from "@app/views/Project/MembersPage";
|
||||||
import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog";
|
|
||||||
import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable";
|
|
||||||
import guidGenerator from "@app/components/utilities/randomId";
|
|
||||||
import { Input } from "@app/components/v2";
|
|
||||||
import { useOrganization } from "@app/context";
|
|
||||||
import {
|
|
||||||
useAddUserToWorkspace,
|
|
||||||
useGetOrgUsers,
|
|
||||||
useGetUser,
|
|
||||||
useGetWorkspaceUsers} from "@app/hooks/api";
|
|
||||||
import { uploadWsKey } from "@app/hooks/api/keys/queries";
|
|
||||||
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric
|
|
||||||
} from "../../../../components/utilities/cryptography/crypto";
|
|
||||||
|
|
||||||
interface UserProps {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
email: string;
|
|
||||||
_id: string;
|
|
||||||
publicKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MembershipProps {
|
|
||||||
deniedPermissions: any[];
|
|
||||||
user: UserProps;
|
|
||||||
inviteEmail: string;
|
|
||||||
role: string;
|
|
||||||
status: string;
|
|
||||||
_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// #TODO: Update all the workspaceIds
|
|
||||||
|
|
||||||
export default function Users() {
|
|
||||||
const router = useRouter();
|
|
||||||
const workspaceId = router.query.id as string;
|
|
||||||
|
|
||||||
const { data: user } = useGetUser();
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { data: orgUsers } = useGetOrgUsers(currentOrg?._id ?? "");
|
|
||||||
|
|
||||||
const { data: workspaceUsers } = useGetWorkspaceUsers(workspaceId);
|
|
||||||
const { mutateAsync: addUserToWorkspaceMutateAsync } = useAddUserToWorkspace();
|
|
||||||
|
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
|
||||||
// let [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
|
||||||
// let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false);
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [personalEmail, setPersonalEmail] = useState("");
|
|
||||||
const [searchUsers, setSearchUsers] = useState("");
|
|
||||||
|
|
||||||
|
export default function WorkspaceMemberSettings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
const [userList, setUserList] = useState<any[]>([]);
|
<>
|
||||||
const [isUserListLoading, setIsUserListLoading] = useState(true);
|
|
||||||
const [orgUserList, setOrgUserList] = useState<any[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user && workspaceUsers && orgUsers) {
|
|
||||||
(async () => {
|
|
||||||
setPersonalEmail(user.email);
|
|
||||||
|
|
||||||
const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({
|
|
||||||
key: guidGenerator(),
|
|
||||||
firstName: membership.user?.firstName,
|
|
||||||
lastName: membership.user?.lastName,
|
|
||||||
email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email,
|
|
||||||
role: membership?.role,
|
|
||||||
status: membership?.status,
|
|
||||||
userId: membership.user?._id,
|
|
||||||
membershipId: membership._id,
|
|
||||||
deniedPermissions: membership.deniedPermissions,
|
|
||||||
publicKey: membership.user?.publicKey
|
|
||||||
}));
|
|
||||||
setUserList(tempUserList);
|
|
||||||
|
|
||||||
setIsUserListLoading(false);
|
|
||||||
|
|
||||||
setOrgUserList(orgUsers);
|
|
||||||
setEmail(
|
|
||||||
orgUsers
|
|
||||||
?.filter((membership: MembershipProps) => membership.status === "accepted")
|
|
||||||
.map((membership: MembershipProps) => membership.user.email)
|
|
||||||
.filter(
|
|
||||||
(usEmail: string) =>
|
|
||||||
!tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail)
|
|
||||||
)[0]
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}, [user, workspaceUsers, orgUsers]);
|
|
||||||
|
|
||||||
const closeAddModal = () => {
|
|
||||||
setIsAddOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddModal = () => {
|
|
||||||
setIsAddOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// function closeDeleteModal() {
|
|
||||||
// setIsDeleteOpen(false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function deleteMembership(userId) {
|
|
||||||
// deleteUserFromWorkspace(userId, router.query.id)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function openDeleteModal() {
|
|
||||||
// setIsDeleteOpen(true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const submitAddModal = async () => {
|
|
||||||
const result = await addUserToWorkspaceMutateAsync({
|
|
||||||
email,
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.invitee && result?.latestKey) {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
|
||||||
|
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
|
||||||
const key = decryptAssymmetric({
|
|
||||||
ciphertext: result.latestKey.encryptedKey,
|
|
||||||
nonce: result.latestKey.nonce,
|
|
||||||
publicKey: result.latestKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
publicKey: result.invitee.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadWsKey({
|
|
||||||
workspaceId,
|
|
||||||
userId: result.invitee._id,
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setEmail("");
|
|
||||||
setIsAddOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return userList ? (
|
|
||||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
<MembersPage />
|
||||||
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
|
</>
|
||||||
</div>
|
|
||||||
<AddProjectMemberDialog
|
|
||||||
isOpen={isAddOpen}
|
|
||||||
closeModal={closeAddModal}
|
|
||||||
submitModal={submitAddModal}
|
|
||||||
email={email}
|
|
||||||
data={orgUserList
|
|
||||||
?.filter((membership: MembershipProps) => membership.status === "accepted")
|
|
||||||
.map((membership: MembershipProps) => membership.user.email)
|
|
||||||
.filter(
|
|
||||||
(orgEmail) => !userList?.map((user1: UserProps) => user1.email).includes(orgEmail)
|
|
||||||
)}
|
|
||||||
setEmail={setEmail}
|
|
||||||
/>
|
|
||||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
|
||||||
<div className="flex w-full flex-row items-start px-6 pb-1">
|
|
||||||
<div className="flex w-full max-w-sm flex flex-row ml-auto">
|
|
||||||
<Input
|
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
|
||||||
placeholder="Search by users..."
|
|
||||||
value={searchUsers}
|
|
||||||
onChange={(e) => setSearchUsers(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ml-2 flex min-w-max flex-row items-start justify-start">
|
|
||||||
<Button
|
|
||||||
text={String(t("section.members.add-member"))}
|
|
||||||
onButtonPressed={() => {
|
|
||||||
openAddModal();
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
icon={faPlus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="block overflow-x-auto px-6 pb-6">
|
|
||||||
<ProjectUsersTable
|
|
||||||
userData={userList}
|
|
||||||
changeData={setUserList}
|
|
||||||
myUser={personalEmail}
|
|
||||||
filter={searchUsers}
|
|
||||||
isUserListLoading={isUserListLoading}
|
|
||||||
// onClick={openDeleteModal}
|
|
||||||
// deleteUser={deleteMembership}
|
|
||||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="relative z-10 mr-auto ml-2 flex h-full w-10/12 flex-col items-center justify-center bg-bunker-800">
|
|
||||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Users.requireAuth = true;
|
WorkspaceMemberSettings.requireAuth = true;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { useGetRoles } from "@app/hooks/api";
|
import { useGetRoles } from "@app/hooks/api";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
import { OrgMembersTable } from "./components/OrgMembersTable";
|
import { OrgMembersTable } from "./components/OrgMembersTable";
|
||||||
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
|
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
|
||||||
@@ -47,16 +48,16 @@ export const MembersPage = withPermission(
|
|||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<OrgMembersTable roles={roles} />
|
<OrgMembersTable roles={roles as TRole<undefined>[]} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={TabSections.Roles}>
|
<TabPanel value={TabSections.Roles}>
|
||||||
<OrgRoleTabSection roles={roles} />
|
<OrgRoleTabSection roles={roles as TRole<undefined>[]} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Member }
|
{ action: GeneralPermissionActions.Read, subject: OrgPermissionSubjects.Member }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgGeneralPermissionActions,
|
GeneralPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
@@ -65,7 +65,7 @@ import { TRole } from "@app/hooks/api/roles/types";
|
|||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roles?: TRole[];
|
roles?: TRole<undefined>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
const addMemberFormSchema = yup.object({
|
||||||
@@ -305,7 +305,7 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
|||||||
placeholder="Search members..."
|
placeholder="Search members..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
@@ -359,7 +359,7 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
|||||||
<Td>{email}</Td>
|
<Td>{email}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Edit}
|
I={GeneralPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.Member}
|
a={OrgPermissionSubjects.Member}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -453,7 +453,7 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
|||||||
<Td>
|
<Td>
|
||||||
{userId !== u?._id && (
|
{userId !== u?._id && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Delete}
|
I={GeneralPermissionActions.Delete}
|
||||||
a={OrgPermissionSubjects.Member}
|
a={OrgPermissionSubjects.Member}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ import { SsoPermission } from "./SsoPermission";
|
|||||||
import { WorkspacePermission } from "./WorkspacePermission";
|
import { WorkspacePermission } from "./WorkspacePermission";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role?: TRole;
|
role?: TRole<undefined>;
|
||||||
onGoBack: VoidFunction;
|
onGoBack: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -1,14 +1,13 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
import { TRole } from "~/hooks/api/roles/types";
|
|
||||||
|
|
||||||
import { OrgRoleModifySection } from "./OrgRoleModifySection";
|
import { OrgRoleModifySection } from "./OrgRoleModifySection";
|
||||||
import { OrgRoleTable } from "./OrgRoleTable";
|
import { OrgRoleTable } from "./OrgRoleTable";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roles?: TRole[];
|
roles?: TRole<undefined>[];
|
||||||
isRolesLoading?: boolean;
|
isRolesLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => {
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<OrgRoleModifySection
|
<OrgRoleModifySection
|
||||||
role={popUp.editRole.data as TRole}
|
role={popUp.editRole.data as TRole<undefined>}
|
||||||
onGoBack={() => handlePopUpClose("editRole")}
|
onGoBack={() => handlePopUpClose("editRole")}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-so
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
@@ -19,16 +20,14 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteRole } from "@app/hooks/api";
|
import { useDeleteRole } from "@app/hooks/api";
|
||||||
import { TRole } from "@app/hooks/api/roles/types";
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
|
|
||||||
import { usePopUp } from "~/hooks/usePopUp";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isRolesLoading?: boolean;
|
isRolesLoading?: boolean;
|
||||||
roles?: TRole[];
|
roles?: TRole<undefined>[];
|
||||||
onSelectRole: (role?: TRole) => void;
|
onSelectRole: (role?: TRole<undefined>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
|
export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
|
||||||
@@ -41,7 +40,7 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
|||||||
const { mutateAsync: deleteRole } = useDeleteRole();
|
const { mutateAsync: deleteRole } = useDeleteRole();
|
||||||
|
|
||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { _id: id } = popUp?.deleteRole?.data as TRole;
|
const { _id: id } = popUp?.deleteRole?.data as TRole<undefined>;
|
||||||
try {
|
try {
|
||||||
await deleteRole({
|
await deleteRole({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -129,9 +128,9 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
|||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteRole.isOpen}
|
isOpen={popUp.deleteRole.isOpen}
|
||||||
title={`Are you sure want to delete ${
|
title={`Are you sure want to delete ${
|
||||||
(popUp?.deleteRole?.data as TRole)?.name || " "
|
(popUp?.deleteRole?.data as TRole<undefined>)?.name || " "
|
||||||
} role?`}
|
} role?`}
|
||||||
deleteKey={(popUp?.deleteRole?.data as TRole)?.slug || ""}
|
deleteKey={(popUp?.deleteRole?.data as TRole<undefined>)?.slug || ""}
|
||||||
onClose={() => handlePopUpClose("deleteRole")}
|
onClose={() => handlePopUpClose("deleteRole")}
|
||||||
onDeleteApproved={handleRoleDelete}
|
onDeleteApproved={handleRoleDelete}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useGetRoles } from "@app/hooks/api";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
import { MemberListTab } from "./components/MemberListTab";
|
||||||
|
import { ProjectRoleListTab } from "./components/ProjectRoleListTab";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Member = "members",
|
||||||
|
Roles = "roles"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MembersPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
const orgId = currentWorkspace?.organization || "";
|
||||||
|
|
||||||
|
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||||
|
orgId,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
||||||
|
<p className="mr-4 mb-4 text-3xl font-semibold text-white">{t("settings.members.title")}</p>
|
||||||
|
<Tabs defaultValue={TabSections.Member}>
|
||||||
|
<TabList>
|
||||||
|
<Tab value={TabSections.Member}>Members</Tab>
|
||||||
|
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
|
||||||
|
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||||
|
)}
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Member}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<MemberListTab roles={roles as TRole<string>[]} />
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Roles}>
|
||||||
|
<ProjectRoleListTab roles={roles as TRole<string>[]} isRolesLoading={isRolesLoading} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
GeneralPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useUser,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useAddUserToWs,
|
||||||
|
useDeleteUserFromWorkspace,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useGetWorkspaceUsers,
|
||||||
|
useUpdateUserWorkspaceRole,
|
||||||
|
useUploadWsKey
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
roles?: TRole<string>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMemberFormSchema = z.object({
|
||||||
|
email: z.string().email().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
|
export const MemberListTab = ({ roles = [] }: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const userId = user?._id || "";
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
|
||||||
|
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||||
|
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
|
||||||
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addMember",
|
||||||
|
"removeMember",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
|
const { mutateAsync: addUserToWorkspace } = useAddUserToWs();
|
||||||
|
const { mutateAsync: uploadWsKey } = useUploadWsKey();
|
||||||
|
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||||
|
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addUserToWorkspace({
|
||||||
|
email,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully invited user to the organization.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to invite user to org",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handlePopUpClose("addMember");
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveUser = async () => {
|
||||||
|
const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeUserFromWorkspace(membershipId);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed user from workspace",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove user from the organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handlePopUpClose("removeMember");
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIamOwner = useMemo(
|
||||||
|
() => members?.find(({ user: u }) => userId === u?._id)?.role === "owner",
|
||||||
|
[userId, members]
|
||||||
|
);
|
||||||
|
|
||||||
|
const findRoleFromId = useCallback(
|
||||||
|
(roleId: string) => {
|
||||||
|
return roles.find(({ _id: id }) => id === roleId);
|
||||||
|
},
|
||||||
|
[roles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRoleChange = async (membershipId: string, role: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateUserWorkspaceRole({ membershipId, role });
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated user role",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update user role",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterdUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
members?.filter(
|
||||||
|
({ user: u, inviteEmail }) =>
|
||||||
|
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
u?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
|
),
|
||||||
|
[members, searchMemberFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredOrgUsers = useMemo(() => {
|
||||||
|
const wsUserEmails = new Map();
|
||||||
|
members?.forEach((member) => {
|
||||||
|
wsUserEmails.set(member.user.email, true);
|
||||||
|
});
|
||||||
|
return (orgUsers || []).filter(
|
||||||
|
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email)
|
||||||
|
);
|
||||||
|
}, [orgUsers, members]);
|
||||||
|
|
||||||
|
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
|
||||||
|
try {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
if (!PRIVATE_KEY || !wsKey) return;
|
||||||
|
|
||||||
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: wsKey.encryptedKey,
|
||||||
|
nonce: wsKey.nonce,
|
||||||
|
publicKey: wsKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadWsKey({
|
||||||
|
userId: grantedUserId,
|
||||||
|
nonce,
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
workspaceId: currentWorkspace?._id || ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to grant access to user",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = isMembersLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchMemberFilter}
|
||||||
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search members..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("addMember")}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th>Role</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
|
||||||
|
{!isLoading &&
|
||||||
|
filterdUsers?.map(
|
||||||
|
({ user: u, inviteEmail, _id: membershipId, status, customRole, role }) => {
|
||||||
|
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
||||||
|
const email = u?.email || inviteEmail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`membership-${membershipId}`} className="w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={GeneralPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
defaultValue={
|
||||||
|
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||||
|
}
|
||||||
|
isDisabled={userId === u?._id || !isAllowed}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(membershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{roles
|
||||||
|
.filter(({ slug }) =>
|
||||||
|
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||||
|
)
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{status === "completed" && user.email !== email && (
|
||||||
|
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
|
||||||
|
>
|
||||||
|
Grant Access
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== u?._id && (
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={GeneralPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
isDisabled={userId === u?._id || !isAllowed}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("removeMember", { id: membershipId })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && filterdUsers?.length === 0 && (
|
||||||
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={t("section.members.add-dialog.add-member-to-project") as string}
|
||||||
|
subTitle={t("section.members.add-dialog.user-will-email")}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={filteredOrgUsers?.[0]?.user?.email}
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Select
|
||||||
|
position="popper"
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={filteredOrgUsers?.[0]?.user?.email}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
{filteredOrgUsers.map(({ _id: orgUserId, user: u }) => (
|
||||||
|
<SelectItem value={u?.email} key={`org-membership-join-${orgUserId}`}>
|
||||||
|
{u?.email}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addMember")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeMember.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this user from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
|
onDeleteApproved={handleRemoveUser}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { MemberListTab } from "./MemberListTab";
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
import { ProjectRoleList } from "./components/ProjectRoleList";
|
||||||
|
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
roles?: TRole<string>[];
|
||||||
|
isRolesLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||||
|
|
||||||
|
return popUp.editRole.isOpen ? (
|
||||||
|
<motion.div
|
||||||
|
key="role-modify"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<ProjectRoleModifySection
|
||||||
|
role={popUp.editRole.data as TRole<string>}
|
||||||
|
onGoBack={() => handlePopUpClose("editRole")}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="role-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<ProjectRoleList
|
||||||
|
roles={roles}
|
||||||
|
isRolesLoading={isRolesLoading}
|
||||||
|
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteRole } from "@app/hooks/api";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isRolesLoading?: boolean;
|
||||||
|
roles?: TRole<string>[];
|
||||||
|
onSelectRole: (role?: TRole<string>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
|
||||||
|
const [searchRoles, setSearchRoles] = useState("");
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteRole } = useDeleteRole();
|
||||||
|
|
||||||
|
const handleRoleDelete = async () => {
|
||||||
|
const { _id: id } = popUp?.deleteRole?.data as TRole<string>;
|
||||||
|
try {
|
||||||
|
await deleteRole({
|
||||||
|
orgId,
|
||||||
|
workspaceId,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||||
|
handlePopUpClose("deleteRole");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to create role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchRoles}
|
||||||
|
onChange={(e) => setSearchRoles(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search roles..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
|
||||||
|
Add Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Slug</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||||
|
{roles?.map((role) => {
|
||||||
|
const { _id: id, name, createdAt, slug } = role;
|
||||||
|
const isNonMutatable = ["owner", "admin", "member"].includes(slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`role-list-${id}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{slug}</Td>
|
||||||
|
<Td>
|
||||||
|
{createdAt ? format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa") : "-"}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<Tooltip content="Edit">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit"
|
||||||
|
onClick={() => onSelectRole(role)}
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||||
|
variant="plain"
|
||||||
|
isDisabled={isNonMutatable}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteRole.isOpen}
|
||||||
|
title={`Are you sure want to delete ${
|
||||||
|
(popUp?.deleteRole?.data as TRole<string>)?.name || " "
|
||||||
|
} role?`}
|
||||||
|
deleteKey={(popUp?.deleteRole?.data as TRole<string>)?.slug || ""}
|
||||||
|
onClose={() => handlePopUpClose("deleteRole")}
|
||||||
|
onDeleteApproved={handleRoleDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectRoleList } from "./ProjectRoleList";
|
||||||
+236
@@ -0,0 +1,236 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
formName: "secrets" | "folders" | "secret-imports";
|
||||||
|
isNonEditable?: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
icon: IconProp;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
ReadOnly = "read-only",
|
||||||
|
FullAccess = "full-acess",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiEnvProjectPermission = ({
|
||||||
|
isNonEditable,
|
||||||
|
setValue,
|
||||||
|
control,
|
||||||
|
formName,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
icon
|
||||||
|
}: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
const customRule = useWatch({
|
||||||
|
control,
|
||||||
|
name: `permissions.${formName}.custom`
|
||||||
|
});
|
||||||
|
const isCustom = Boolean(customRule);
|
||||||
|
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
const { read, delete: del, edit, create } = allRule || {};
|
||||||
|
if (read && del && edit && create) return Permission.FullAccess;
|
||||||
|
if (read) return Permission.ReadOnly;
|
||||||
|
return Permission.NoAccess;
|
||||||
|
}, [allRule]);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
switch (val) {
|
||||||
|
case Permission.NoAccess:
|
||||||
|
setValue(`permissions.${formName}`, {}, { shouldDirty: true });
|
||||||
|
break;
|
||||||
|
case Permission.FullAccess:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ all: { read: true, edit: true, create: true, delete: true } },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Permission.ReadOnly:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ all: { read: true, edit: false, create: false, delete: false } },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ custom: { read: false, edit: false, create: false, delete: false } },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"px-10 py-6 bg-mineshaft-800 rounded-md",
|
||||||
|
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
|
||||||
|
"border-l-2 border-primary-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={icon} className="text-4xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
<div className="font-medium mb-1 text-lg">{title}</div>
|
||||||
|
<div className="text-xs font-light">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
defaultValue={Permission.NoAccess}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
value={isCustom ? Permission.Custom : selectedPermissionCategory}
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||||
|
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{ height: isCustom ? "auto" : 0 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<TableContainer className="border-mineshaft-500 mt-6">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th />
|
||||||
|
<Th className="min-w-[8rem]">Secret Path</Th>
|
||||||
|
<Th className="text-center">Read</Th>
|
||||||
|
<Th className="text-center">Create</Th>
|
||||||
|
<Th className="text-center">Edit</Th>
|
||||||
|
<Th className="text-center">Delete</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isCustom &&
|
||||||
|
environments.map(({ name, slug }) => (
|
||||||
|
<Tr key={`custom-role-project-secret-${slug}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`permissions.${formName}.${slug}.secretPath`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input {...field} className="w-full overflow-ellipsis" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`permissions.${formName}.${slug}.read`}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.${formName}.${slug}.read`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`permissions.${formName}.${slug}.create`}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
id={`permissions.${formName}.${slug}.modify`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`permissions.${formName}.${slug}.edit`}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
id={`permissions.${formName}.${slug}.modify`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
defaultValue={false}
|
||||||
|
name={`permissions.${formName}.${slug}.delete`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.${formName}.${slug}.delete`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+290
@@ -0,0 +1,290 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { faElementor } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import {
|
||||||
|
faAnchorLock,
|
||||||
|
faArrowLeft,
|
||||||
|
faBook,
|
||||||
|
faCog,
|
||||||
|
faFolder,
|
||||||
|
faKey,
|
||||||
|
faLink,
|
||||||
|
faLock,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faNetworkWired,
|
||||||
|
faPuzzlePiece,
|
||||||
|
faTags,
|
||||||
|
faUser,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
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 { useOrganization, useWorkspace } from "@app/context";
|
||||||
|
import { useCreateRole, useUpdateRole } from "@app/hooks/api";
|
||||||
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
|
||||||
|
import {
|
||||||
|
formRolePermission2API,
|
||||||
|
formSchema,
|
||||||
|
rolePermission2Form,
|
||||||
|
TFormSchema
|
||||||
|
} from "./ProjectRoleModifySection.utils";
|
||||||
|
import { SingleProjectPermission } from "./SingleProjectPermission";
|
||||||
|
|
||||||
|
const SINGLE_PERMISSION_LIST = [
|
||||||
|
{
|
||||||
|
title: "Integrations",
|
||||||
|
subtitle: "Integration management control",
|
||||||
|
icon: faPuzzlePiece,
|
||||||
|
formName: "integrations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Roles",
|
||||||
|
subtitle: "Role management control",
|
||||||
|
icon: faUsers,
|
||||||
|
formName: "role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Project Members",
|
||||||
|
subtitle: "Project members management control",
|
||||||
|
icon: faUser,
|
||||||
|
formName: "member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Webhooks",
|
||||||
|
subtitle: "Webhook management control",
|
||||||
|
icon: faAnchorLock,
|
||||||
|
formName: "webhooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Service Tokens",
|
||||||
|
subtitle: "Token management control",
|
||||||
|
icon: faKey,
|
||||||
|
formName: "service-tokens"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
subtitle: "Settings control",
|
||||||
|
icon: faCog,
|
||||||
|
formName: "settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Environments",
|
||||||
|
subtitle: "Environment management control",
|
||||||
|
icon: faElementor,
|
||||||
|
formName: "environments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tags",
|
||||||
|
subtitle: "Tag management control",
|
||||||
|
icon: faTags,
|
||||||
|
formName: "tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Audit Logs",
|
||||||
|
subtitle: "Audit log management control",
|
||||||
|
icon: faBook,
|
||||||
|
formName: "audit-logs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "IP Allowlist",
|
||||||
|
subtitle: "IP allowlist management control",
|
||||||
|
icon: faNetworkWired,
|
||||||
|
formName: "ip-allowlist"
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
role?: TRole<string>;
|
||||||
|
onGoBack: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||||
|
const [searchPermission, setSearchPermission] = useState("");
|
||||||
|
|
||||||
|
const isNonEditable = ["owner", "admin", "member"].includes(role?.slug || "");
|
||||||
|
const isNewRole = !role?.slug;
|
||||||
|
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?._id || "";
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isSubmitting, isDirty, errors },
|
||||||
|
setValue,
|
||||||
|
control
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
const { mutateAsync: createRole } = useCreateRole();
|
||||||
|
const { mutateAsync: updateRole } = useUpdateRole();
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||||
|
if (!role?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateRole({
|
||||||
|
orgId,
|
||||||
|
id: role?._id,
|
||||||
|
workspaceId,
|
||||||
|
...el,
|
||||||
|
permissions: formRolePermission2API(el.permissions)
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully updated role" });
|
||||||
|
onGoBack();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to update role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (el: TFormSchema) => {
|
||||||
|
if (!isNewRole) {
|
||||||
|
await handleRoleUpdate(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createRole({
|
||||||
|
orgId,
|
||||||
|
workspaceId,
|
||||||
|
...el,
|
||||||
|
permissions: formRolePermission2API(el.permissions)
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Created new role" });
|
||||||
|
onGoBack();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to create role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<div className="flex justify-between mb-2 items-center">
|
||||||
|
<h1 className="text-xl font-semibold text-mineshaft-100">
|
||||||
|
{isNewRole ? "New" : "Edit"} Role
|
||||||
|
</h1>
|
||||||
|
<Button
|
||||||
|
onClick={onGoBack}
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="mb-8 text-gray-400">
|
||||||
|
Roles are used to grant access to particular resources in your organization
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
<FormControl
|
||||||
|
label="Name"
|
||||||
|
isRequired
|
||||||
|
className="mb-0"
|
||||||
|
isError={Boolean(errors?.name)}
|
||||||
|
errorText={errors?.name?.message}
|
||||||
|
>
|
||||||
|
<Input {...register("name")} placeholder="Billing Team" isReadOnly={isNonEditable} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Slug"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(errors?.slug)}
|
||||||
|
errorText={errors?.slug?.message}
|
||||||
|
>
|
||||||
|
<Input {...register("slug")} placeholder="biller" isReadOnly={isNonEditable} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Description"
|
||||||
|
helperText="A short description about this role"
|
||||||
|
isError={Boolean(errors?.description)}
|
||||||
|
errorText={errors?.description?.message}
|
||||||
|
>
|
||||||
|
<Input {...register("description")} isReadOnly={isNonEditable} />
|
||||||
|
</FormControl>
|
||||||
|
<div className="flex justify-between items-center pt-4 border-t border-t-mineshaft-800">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium">Add Permission</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 max-w-md">
|
||||||
|
<Input
|
||||||
|
value={searchPermission}
|
||||||
|
onChange={(e) => setSearchPermission(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search permissions..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<MultiEnvProjectPermission
|
||||||
|
isNonEditable={isNonEditable}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
icon={faLock}
|
||||||
|
title="Secrets"
|
||||||
|
subtitle="Secret management control"
|
||||||
|
formName="secrets"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<MultiEnvProjectPermission
|
||||||
|
isNonEditable={isNonEditable}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
icon={faFolder}
|
||||||
|
title="Folders"
|
||||||
|
subtitle="Folder management control"
|
||||||
|
formName="folders"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<MultiEnvProjectPermission
|
||||||
|
isNonEditable={isNonEditable}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
icon={faLink}
|
||||||
|
title="Secret Imports"
|
||||||
|
subtitle="Secret import management control"
|
||||||
|
formName="secret-imports"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
|
||||||
|
<div className="flex flex-col space-y-4" key={`permission-${title}`}>
|
||||||
|
<SingleProjectPermission
|
||||||
|
isNonEditable={isNonEditable}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
icon={icon}
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
formName={formName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4 mt-12">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting || isNonEditable || !isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
{isNewRole ? "Create Role" : "Save Role"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onGoBack} variant="outline_bg">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
const generalPermissionSchema = z
|
||||||
|
.object({
|
||||||
|
read: z.boolean().optional(),
|
||||||
|
edit: z.boolean().optional(),
|
||||||
|
delete: z.boolean().optional(),
|
||||||
|
create: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
const multiEnvPermissionSchema = z
|
||||||
|
.object({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
read: z.boolean().optional(),
|
||||||
|
edit: z.boolean().optional(),
|
||||||
|
delete: z.boolean().optional(),
|
||||||
|
create: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
|
||||||
|
const MULTI_ENV_KEY = ["secrets", "folders", "secret-imports"] as const;
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
slug: z.string(),
|
||||||
|
permissions: z.object({
|
||||||
|
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||||
|
folders: z.record(multiEnvPermissionSchema).optional(),
|
||||||
|
"secret-imports": z.record(multiEnvPermissionSchema).optional(),
|
||||||
|
member: generalPermissionSchema,
|
||||||
|
role: generalPermissionSchema,
|
||||||
|
integrations: generalPermissionSchema,
|
||||||
|
webhooks: generalPermissionSchema,
|
||||||
|
"service-tokens": generalPermissionSchema,
|
||||||
|
settings: generalPermissionSchema,
|
||||||
|
environments: generalPermissionSchema,
|
||||||
|
tags: generalPermissionSchema,
|
||||||
|
"audit-logs": generalPermissionSchema,
|
||||||
|
"ip-allowlist": generalPermissionSchema,
|
||||||
|
workspace: z
|
||||||
|
.object({
|
||||||
|
edit: z.boolean().optional(),
|
||||||
|
delete: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const multiEnvApi2Form = (
|
||||||
|
formVal: TFormSchema["permissions"]["secrets"],
|
||||||
|
permission: TProjectPermission
|
||||||
|
) => {
|
||||||
|
const isCustomRule = Boolean(permission?.condition?.slug);
|
||||||
|
// full access
|
||||||
|
if (isCustomRule && formVal && !formVal?.custom) {
|
||||||
|
formVal.custom = { read: true, edit: true, delete: true, create: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretEnv = permission?.condition?.slug || "all";
|
||||||
|
const secretPath = permission?.condition?.secretPath;
|
||||||
|
// initialize
|
||||||
|
if (formVal && !formVal?.[secretEnv]) {
|
||||||
|
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
||||||
|
}
|
||||||
|
formVal![secretEnv]![permission.action] = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// convert role permission to form compatiable data structure
|
||||||
|
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||||
|
const formVal: TFormSchema["permissions"] = {
|
||||||
|
secrets: {},
|
||||||
|
folders: {},
|
||||||
|
integrations: {},
|
||||||
|
settings: {},
|
||||||
|
role: {},
|
||||||
|
member: {},
|
||||||
|
"service-tokens": {},
|
||||||
|
workspace: {},
|
||||||
|
environments: {},
|
||||||
|
tags: {},
|
||||||
|
webhooks: {},
|
||||||
|
"audit-logs": {},
|
||||||
|
"ip-allowlist": {},
|
||||||
|
"secret-imports": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
permissions.forEach((permission) => {
|
||||||
|
if (["secrets", "folders", "secret-imports"].includes(permission.subject)) {
|
||||||
|
multiEnvApi2Form(formVal?.secrets, permission);
|
||||||
|
} else {
|
||||||
|
// everything else follows same pattern
|
||||||
|
// formVal[settings][read | write] = true
|
||||||
|
const key = permission.subject as keyof Omit<
|
||||||
|
TFormSchema["permissions"],
|
||||||
|
"secrets" | "workspace"
|
||||||
|
>;
|
||||||
|
formVal[key]![permission.action] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiEnvForm2Api = (
|
||||||
|
permissions: TProjectPermission[],
|
||||||
|
formVal: TFormSchema["permissions"]["secrets"],
|
||||||
|
subject: (typeof MULTI_ENV_KEY)[number]
|
||||||
|
) => {
|
||||||
|
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||||
|
// if any of them is set in all push it without any condition
|
||||||
|
PERMISSION_ACTIONS.forEach((action) => {
|
||||||
|
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFullAccess) {
|
||||||
|
Object.keys(formVal || {})
|
||||||
|
.filter((id) => id !== "all" && id !== "custom") // remove all and custom for iter
|
||||||
|
.forEach((slug) => {
|
||||||
|
const actions = Object.keys(formVal?.[slug] || {}) as [
|
||||||
|
"read",
|
||||||
|
"edit",
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"secretPath"
|
||||||
|
];
|
||||||
|
actions.forEach((action) => {
|
||||||
|
// if not full access for an action
|
||||||
|
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
|
||||||
|
permissions.push({
|
||||||
|
action,
|
||||||
|
subject,
|
||||||
|
condition: { slug, secretPath: formVal[slug]?.secretPath }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
|
const permissions: TProjectPermission[] = [];
|
||||||
|
MULTI_ENV_KEY.forEach((formName) => {
|
||||||
|
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal[formName] || {})), formName);
|
||||||
|
});
|
||||||
|
// other than workspace everything else follows same
|
||||||
|
// if in future there is a different follow the above on how workspace is done
|
||||||
|
(Object.keys(formVal) as Array<keyof typeof formVal>)
|
||||||
|
.filter((key) => !["secret-imports", "folders", "secrets"].includes(key))
|
||||||
|
.forEach((rule) => {
|
||||||
|
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
|
||||||
|
// quite annoying i know
|
||||||
|
const actions = Object.keys(formVal[rule] || {}) as Array<
|
||||||
|
keyof z.infer<typeof generalPermissionSchema>
|
||||||
|
>;
|
||||||
|
actions.forEach((action) => {
|
||||||
|
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
|
||||||
|
if (formVal[rule]?.[action as keyof typeof formVal.workspace]) {
|
||||||
|
permissions.push({ subject: rule, action } as any);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
formName:
|
||||||
|
| "role"
|
||||||
|
| "member"
|
||||||
|
| "integrations"
|
||||||
|
| "webhooks"
|
||||||
|
| "service-tokens"
|
||||||
|
| "settings"
|
||||||
|
| "environments"
|
||||||
|
| "tags"
|
||||||
|
| "audit-logs"
|
||||||
|
| "ip-allowlist";
|
||||||
|
isNonEditable?: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
icon: IconProp;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
ReadOnly = "read-only",
|
||||||
|
FullAccess = "full-acess",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSIONS = [
|
||||||
|
{ action: "read", label: "Read" },
|
||||||
|
{ action: "create", label: "Create" },
|
||||||
|
{ action: "edit", label: "Update" },
|
||||||
|
{ action: "delete", label: "Remove" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SingleProjectPermission = ({
|
||||||
|
isNonEditable,
|
||||||
|
setValue,
|
||||||
|
control,
|
||||||
|
formName,
|
||||||
|
subtitle,
|
||||||
|
title,
|
||||||
|
icon
|
||||||
|
}: Props) => {
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: `permissions.${formName}`
|
||||||
|
});
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||||
|
const totalActions = PERMISSIONS.length;
|
||||||
|
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||||
|
|
||||||
|
if (isCustom) return Permission.Custom;
|
||||||
|
if (score === 0) return Permission.NoAccess;
|
||||||
|
if (score === totalActions) return Permission.FullAccess;
|
||||||
|
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
||||||
|
|
||||||
|
return Permission.Custom;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (val === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
|
||||||
|
switch (val) {
|
||||||
|
case Permission.NoAccess:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ read: false, edit: false, create: false, delete: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Permission.FullAccess:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ read: true, edit: true, create: true, delete: true },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Permission.ReadOnly:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ read: true, edit: false, create: false, delete: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{ read: false, edit: false, create: false, delete: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"px-10 py-6 bg-mineshaft-800 rounded-md",
|
||||||
|
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={icon} className="text-4xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
<div className="font-medium mb-1 text-lg">{title}</div>
|
||||||
|
<div className="text-xs font-light">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
defaultValue={Permission.NoAccess}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||||
|
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||||
|
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
||||||
|
>
|
||||||
|
{isCustom &&
|
||||||
|
PERMISSIONS.map(({ action, label }) => (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.${formName}.${action}`}
|
||||||
|
key={`permissions.${formName}.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.${formName}.${action}`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectRoleModifySection } from "./ProjectRoleModifySection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectRoleListTab } from "./ProjectRoleListTab";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { MembersPage } from "./MembersPage";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
|
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
|
||||||
|
|
||||||
export const RiskStatusSelection = ({
|
export const RiskStatusSelection = ({
|
||||||
@@ -26,7 +26,7 @@ export const RiskStatusSelection = ({
|
|||||||
}, [selectedRiskStatus]);
|
}, [selectedRiskStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<select
|
<select
|
||||||
disabled={!isAllowed}
|
disabled={!isAllowed}
|
||||||
|
|||||||
+3
-9
@@ -1,7 +1,7 @@
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgGeneralPermissionActions,
|
GeneralPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
@@ -81,10 +81,7 @@ export const PreviewSection = () => {
|
|||||||
Unlimited members, projects, RBAC, smart alerts, and so much more
|
Unlimited members, projects, RBAC, smart alerts, and so much more
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||||
I={OrgGeneralPermissionActions.Create}
|
|
||||||
a={OrgPermissionSubjects.Billing}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleUpgradeBtnClick()}
|
onClick={() => handleUpgradeBtnClick()}
|
||||||
@@ -106,10 +103,7 @@ export const PreviewSection = () => {
|
|||||||
subscription.status === "trialing" ? "(Trial)" : ""
|
subscription.status === "trialing" ? "(Trial)" : ""
|
||||||
}`}
|
}`}
|
||||||
</p>
|
</p>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||||
I={OrgGeneralPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Billing}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ import * as yup from "yup";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
@@ -75,7 +75,7 @@ export const CompanyNameSection = () => {
|
|||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ import * as yup from "yup";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
@@ -76,7 +76,7 @@ export const InvoiceEmailSection = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
||||||
|
|
||||||
import { PmtMethodsTable } from "./PmtMethodsTable";
|
import { PmtMethodsTable } from "./PmtMethodsTable";
|
||||||
@@ -27,7 +27,7 @@ export const PmtMethodsSection = () => {
|
|||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
|
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddPmtMethodBtnClick}
|
onClick={handleAddPmtMethodBtnClick}
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||||
|
|
||||||
export const PmtMethodsTable = () => {
|
export const PmtMethodsTable = () => {
|
||||||
@@ -54,7 +54,7 @@ export const PmtMethodsTable = () => {
|
|||||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Delete}
|
I={GeneralPermissionActions.Delete}
|
||||||
a={OrgPermissionSubjects.Billing}
|
a={OrgPermissionSubjects.Billing}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { TaxIDModal } from "./TaxIDModal";
|
import { TaxIDModal } from "./TaxIDModal";
|
||||||
@@ -18,7 +18,7 @@ export const TaxIDSection = () => {
|
|||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
|
<h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handlePopUpOpen("addTaxID")}
|
onClick={() => handlePopUpOpen("addTaxID")}
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||||
|
|
||||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||||
@@ -103,7 +103,7 @@ export const TaxIDTable = () => {
|
|||||||
<Td>{value}</Td>
|
<Td>{value}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Delete}
|
I={GeneralPermissionActions.Delete}
|
||||||
a={OrgPermissionSubjects.Billing}
|
a={OrgPermissionSubjects.Billing}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
|
|
||||||
import { BillingCloudTab } from "../BillingCloudTab";
|
import { BillingCloudTab } from "../BillingCloudTab";
|
||||||
@@ -53,5 +53,5 @@ export const BillingTabGroup = withPermission(
|
|||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
{ action: GeneralPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
|
|
||||||
import { OrgSSOSection } from "./OrgSSOSection";
|
import { OrgSSOSection } from "./OrgSSOSection";
|
||||||
@@ -11,5 +11,5 @@ export const OrgAuthTab = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
|
{ action: GeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgGeneralPermissionActions,
|
GeneralPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
@@ -86,7 +86,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
|
<h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={addSSOBtnClick}
|
onClick={addSSOBtnClick}
|
||||||
@@ -102,7 +102,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
{data && (
|
{data && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
id="enable-saml-sso"
|
id="enable-saml-sso"
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const OrgIncidentContactsSection = withPermission(
|
|||||||
{t("section.incident.incident-contacts")}
|
{t("section.incident.incident-contacts")}
|
||||||
</p>
|
</p>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Create}
|
I={GeneralPermissionActions.Create}
|
||||||
a={OrgPermissionSubjects.IncidentAccount}
|
a={OrgPermissionSubjects.IncidentAccount}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -50,5 +50,5 @@ export const OrgIncidentContactsSection = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
|
{ action: GeneralPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export const OrgIncidentContactsTable = () => {
|
|||||||
<Td className="w-full">{email}</Td>
|
<Td className="w-full">{email}</Td>
|
||||||
<Td className="mr-4">
|
<Td className="mr-4">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgGeneralPermissionActions.Delete}
|
I={GeneralPermissionActions.Delete}
|
||||||
an={OrgPermissionSubjects.IncidentAccount}
|
an={OrgPermissionSubjects.IncidentAccount}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+3
-3
@@ -6,7 +6,7 @@ import * as yup from "yup";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { GeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { useRenameOrg } from "@app/hooks/api";
|
import { useRenameOrg } from "@app/hooks/api";
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ export const OrgNameChangeSection = withPermission(
|
|||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
<OrgPermissionCan I={GeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -85,7 +85,7 @@ export const OrgNameChangeSection = withPermission(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: OrgGeneralPermissionActions.Read,
|
action: GeneralPermissionActions.Read,
|
||||||
subject: OrgPermissionSubjects.Settings,
|
subject: OrgPermissionSubjects.Settings,
|
||||||
containerClassName: "mb-4"
|
containerClassName: "mb-4"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgGeneralPermissionActions,
|
GeneralPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -382,7 +382,7 @@ export const OrgServiceAccountsTable = withPermission(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: OrgGeneralPermissionActions.Read,
|
action: GeneralPermissionActions.Read,
|
||||||
subject: OrgPermissionSubjects.Settings,
|
subject: OrgPermissionSubjects.Settings,
|
||||||
containerClassName: "mb-4"
|
containerClassName: "mb-4"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+7
-7
@@ -8,7 +8,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
"husky": "^8.0.2"
|
"husky": "^8.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
@@ -602,9 +602,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/husky": {
|
"node_modules/husky": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz",
|
||||||
"integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==",
|
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"husky": "lib/bin.js"
|
"husky": "lib/bin.js"
|
||||||
@@ -1576,9 +1576,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz",
|
||||||
"integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==",
|
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ignore": {
|
"ignore": {
|
||||||
|
|||||||
+1
-1
@@ -20,6 +20,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
"husky": "^8.0.2"
|
"husky": "^8.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user