mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 09:28:36 +00:00
feat(rbac): added new permission check for workspace in frontend
This commit is contained in:
@@ -4,7 +4,7 @@ import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import Role from "../../models/role";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
@@ -107,10 +109,6 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
|
||||
|
||||
if (![ADMIN, MEMBER].includes(role)) {
|
||||
throw new Error("Failed to validate role");
|
||||
}
|
||||
|
||||
// validate target membership
|
||||
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
|
||||
"user"
|
||||
@@ -129,9 +127,32 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const oldRole = membershipToChangeRole.role;
|
||||
membershipToChangeRole.role = role;
|
||||
await membershipToChangeRole.save();
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
if (isCustomRole) {
|
||||
const wsRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
if (!wsRole) throw BadRequestError({ message: "Role not found" });
|
||||
const membership = await Membership.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: wsRole
|
||||
});
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
@@ -140,8 +161,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
userId: membershipToChangeRole.user._id.toString(),
|
||||
email: membershipToChangeRole.user.email,
|
||||
oldRole,
|
||||
newRole: membershipToChangeRole.role
|
||||
oldRole: membershipToChangeRole.role,
|
||||
newRole: role
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -150,7 +171,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership: membershipToChangeRole
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
userHasWriteOnlyAbility
|
||||
} from "../../ee/helpers/checkMembershipPermissions";
|
||||
import _ from "lodash";
|
||||
import { BatchSecret } from "../../types/secret";
|
||||
import {
|
||||
getFolderByPath,
|
||||
getFolderIdFromServiceToken,
|
||||
@@ -56,8 +55,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
body: { secretPath, folderId }
|
||||
} = validatedData;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
const createSecrets: any[] = [];
|
||||
const updateSecrets: any[] = [];
|
||||
const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = [];
|
||||
const actions: IAction[] = [];
|
||||
|
||||
@@ -111,7 +110,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
version: 1,
|
||||
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
workspace: workspaceId,
|
||||
folder: folderId,
|
||||
secretBlindIndex,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
@@ -126,7 +125,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
updateSecrets.push({
|
||||
...request.secret,
|
||||
_id: new Types.ObjectId(request.secret._id),
|
||||
_id: request.secret._id,
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
@@ -145,7 +144,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
// handle create secrets
|
||||
let createdSecrets: ISecret[] = [];
|
||||
if (createSecrets.length > 0) {
|
||||
createdSecrets = await Secret.insertMany(createSecrets);
|
||||
createdSecrets = (await Secret.insertMany(createSecrets)) as any;
|
||||
// (EE) add secret versions for new secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: createdSecrets.map((n: any) => {
|
||||
|
||||
+18
-17
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
} from "../models";
|
||||
import { IUser, User } from "../models";
|
||||
import { sendMail } from "./nodemailer";
|
||||
|
||||
/**
|
||||
@@ -12,7 +9,7 @@ import { sendMail } from "./nodemailer";
|
||||
*/
|
||||
export const setupAccount = async ({ email }: { email: string }) => {
|
||||
const user = await new User({
|
||||
email,
|
||||
email
|
||||
}).save();
|
||||
|
||||
return user;
|
||||
@@ -49,11 +46,11 @@ export const completeAccount = async ({
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
verifier
|
||||
}: {
|
||||
userId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
lastName?: string;
|
||||
encryptionVersion: number;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
@@ -66,7 +63,7 @@ export const completeAccount = async ({
|
||||
verifier: string;
|
||||
}) => {
|
||||
const options = {
|
||||
new: true,
|
||||
new: true
|
||||
};
|
||||
const user = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
@@ -82,7 +79,7 @@ export const completeAccount = async ({
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
verifier
|
||||
},
|
||||
options
|
||||
);
|
||||
@@ -100,22 +97,26 @@ export const completeAccount = async ({
|
||||
export const checkUserDevice = async ({
|
||||
user,
|
||||
ip,
|
||||
userAgent,
|
||||
userAgent
|
||||
}: {
|
||||
user: IUser;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
}) => {
|
||||
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
||||
const isDeviceSeen = user.devices.some(
|
||||
(device) => device.ip === ip && device.userAgent === userAgent
|
||||
);
|
||||
|
||||
if (!isDeviceSeen) {
|
||||
// case: unseen login ip detected for user
|
||||
// -> notify user about the sign-in from new ip
|
||||
|
||||
user.devices = user.devices.concat([{
|
||||
user.devices = user.devices.concat([
|
||||
{
|
||||
ip: String(ip),
|
||||
userAgent,
|
||||
}]);
|
||||
userAgent
|
||||
}
|
||||
]);
|
||||
|
||||
await user.save();
|
||||
|
||||
@@ -128,8 +129,8 @@ export const checkUserDevice = async ({
|
||||
email: user.email,
|
||||
timestamp: new Date().toString(),
|
||||
ip,
|
||||
userAgent,
|
||||
},
|
||||
userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { body, param } from "express-validator";
|
||||
import { requireAuth, validateRequest } from "../../middleware";
|
||||
import { requireAuth } from "../../middleware";
|
||||
import { membershipController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
@@ -14,8 +13,6 @@ router.get(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
param("workspaceId").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.validateMembership
|
||||
);
|
||||
|
||||
@@ -25,8 +22,6 @@ router.delete(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
param("membershipId").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.deleteMembership
|
||||
);
|
||||
|
||||
@@ -36,8 +31,6 @@ router.post(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
body("role").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.changeMembershipRole
|
||||
);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export enum ProjectPermissionSub {
|
||||
Folders = "folders"
|
||||
}
|
||||
|
||||
type GenericFields = {
|
||||
type SubjectFields = {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
};
|
||||
@@ -42,17 +42,17 @@ type GenericFields = {
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & GenericFields)
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & GenericFields)
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & GenericFields)
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
|
||||
Vendored
+8
-2
@@ -28,7 +28,13 @@ export interface BatchSecretRequest {
|
||||
}
|
||||
|
||||
export interface BatchSecret {
|
||||
_id: string;
|
||||
version?: number;
|
||||
_id?: string;
|
||||
user?: string;
|
||||
environment: string;
|
||||
workspace?: string;
|
||||
algorithm?: string;
|
||||
keyEncoding?: string;
|
||||
type: "shared" | "personal";
|
||||
secretName: string;
|
||||
secretBlindIndex: string;
|
||||
@@ -42,5 +48,5 @@ export interface BatchSecret {
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[];
|
||||
folder: string
|
||||
folder: string;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export const CompletedAccountSignupV3 = z.object({
|
||||
body: z.object({
|
||||
email: z.string().email().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional().nullish(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
|
||||
@@ -115,8 +115,8 @@ export const GetSecretVersionsV1 = z.object({
|
||||
secretId: z.string().trim()
|
||||
}),
|
||||
query: z.object({
|
||||
offset: z.number(),
|
||||
limit: z.number()
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -175,17 +175,19 @@ const batchUpdateRequestV2 = z.object({
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
tags: z.object({
|
||||
tags: z
|
||||
.object({
|
||||
_id: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
slug: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export const BatchSecretsV2 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
folderId: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().optional(),
|
||||
requests: z
|
||||
|
||||
@@ -130,8 +130,8 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
|
||||
query: z.object({
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
offset: z.number(),
|
||||
limit: z.number()
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -161,8 +161,8 @@ export const GetWorkspaceLogsV1 = z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
query: z.object({
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number(),
|
||||
sortBy: z.string().trim().optional(),
|
||||
userId: z.string().trim().optional(),
|
||||
actionNames: z.string().trim().optional()
|
||||
@@ -178,8 +178,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
|
||||
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
|
||||
startDate: z.string().datetime().nullable().optional(),
|
||||
endDate: z.string().datetime().nullable().optional(),
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number(),
|
||||
actor: z.string().nullish().optional()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -16,6 +16,15 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
|
||||
|
||||
console.log(workspaceId);
|
||||
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 (isLoading && workspaceId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
@@ -29,20 +38,8 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
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}>
|
||||
<ProjectPermissionContext.Provider value={permission!}>
|
||||
{children}
|
||||
</ProjectPermissionContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
|
||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||
export { ProjectGeneralPermissionActions, ProjectPermissionSubjects } from "./types";
|
||||
export { ProjectPermissionActions,ProjectPermissionSub } from "./types";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { MongoAbility } from "@casl/ability";
|
||||
import { ForcedSubject, MongoAbility } from "@casl/ability";
|
||||
|
||||
export enum ProjectGeneralPermissionActions {
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSubjects {
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
Settings = "settings",
|
||||
@@ -21,24 +21,44 @@ export enum ProjectPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretImports = "secret-imports",
|
||||
SecretRollback = "secret-rollback",
|
||||
Folders = "folders"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
};
|
||||
|
||||
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];
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -7,10 +7,11 @@ export {
|
||||
OrgPermissionSubjects,
|
||||
useOrgPermission
|
||||
} from "./OrgPermissionContext";
|
||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||
export {
|
||||
ProjectGeneralPermissionActions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionProvider,
|
||||
ProjectPermissionSubjects,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "./ProjectPermissionContext";
|
||||
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { withPermission } from "./withPermission";
|
||||
export { withProjectPermission } from "./withProjectPermission";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { withProjectPermission } from "./withProjectPermission";
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ComponentType } from "react";
|
||||
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { TProjectPermission, useProjectPermission } from "@app/context";
|
||||
|
||||
type Props<T extends Abilities> = (T extends AbilityTuple
|
||||
? {
|
||||
action: T[0];
|
||||
subject: Extract<T[1], SubjectType>;
|
||||
}
|
||||
: {
|
||||
action: string;
|
||||
subject: string;
|
||||
}) & { className?: string; containerClassName?: string };
|
||||
|
||||
export const withProjectPermission = <T extends {}, J extends TProjectPermission>(
|
||||
Component: ComponentType<T>,
|
||||
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
|
||||
) => {
|
||||
const HOC = (hocProps: T) => {
|
||||
const permission = useProjectPermission();
|
||||
|
||||
// akhilmhdh: Set as any due to casl/react ts type bug
|
||||
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
|
||||
if (permission.cannot(action as any, subject)) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"container h-full mx-auto flex justify-center items-center",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-medium mb-2">Permission Denied</div>
|
||||
<div className="text-sm">
|
||||
You do not have permission to this page. <br /> Kindly contact your organization
|
||||
administrator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component {...hocProps} />;
|
||||
};
|
||||
|
||||
HOC.displayName = "WithProjectPermission";
|
||||
return HOC;
|
||||
};
|
||||
@@ -6,8 +6,9 @@ import { useRouter } from "next/router";
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import EventFilter from "@app/components/basic/EventFilter";
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
|
||||
@@ -23,10 +24,10 @@ interface LogData {
|
||||
};
|
||||
serviceAccount?: {
|
||||
string: string;
|
||||
},
|
||||
};
|
||||
serviceTokenData?: {
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
@@ -60,7 +61,8 @@ interface LogDataPoint {
|
||||
/**
|
||||
* This is the tab that includes all of the user activity logs
|
||||
*/
|
||||
export default function Activity() {
|
||||
const Activity = withProjectPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState("");
|
||||
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
|
||||
@@ -70,9 +72,7 @@ export default function Activity() {
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>();
|
||||
const { t } = useTranslation();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
@@ -186,12 +186,19 @@ export default function Activity() {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
|
||||
text={
|
||||
subscription.slug === null
|
||||
? "You can see more logs under an Enterprise license"
|
||||
: "You can see more logs if you switch to Infisical's Business/Professional Plan."
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
|
||||
Activity.requireAuth = true;
|
||||
Object.assign(Activity, { requireAuth: true });
|
||||
|
||||
export default Activity;
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -55,7 +56,8 @@ import {
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { leaveConfirmDefaultMessage } from "@app/const";
|
||||
import { useOrganization, useSubscription, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub,useOrganization, useSubscription, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useBatchSecretsOp,
|
||||
@@ -123,7 +125,8 @@ type TDeleteSecretImport = { environment: string; secretPath: string };
|
||||
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
||||
* They will get it back
|
||||
*/
|
||||
export const DashboardPage = () => {
|
||||
export const DashboardPage = withProjectPermission(
|
||||
() => {
|
||||
const { subscription } = useSubscription();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -149,7 +152,7 @@ export const DashboardPage = () => {
|
||||
const [snapshotId, setSnaphotId] = useState<string | null>(null);
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||
const deletedSecretIds = useRef<{ id: string; secretName: string; }[]>([]);
|
||||
const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]);
|
||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
||||
|
||||
const folderId = router.query.folderId as string;
|
||||
@@ -297,7 +300,6 @@ export const DashboardPage = () => {
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@@ -311,7 +313,8 @@ export const DashboardPage = () => {
|
||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
||||
const canDoRollback = !isReadOnly && !isAddOnly;
|
||||
const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting;
|
||||
const isSubmitDisabled =
|
||||
isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSnapshotChanging && Boolean(snapshotId)) {
|
||||
@@ -499,27 +502,31 @@ export const DashboardPage = () => {
|
||||
|
||||
// record all deleted ids
|
||||
// This will make final deletion easier
|
||||
const onSecretDelete = useCallback((index: number, secretName: string, id?: string, overrideId?: string) => {
|
||||
if (id) deletedSecretIds.current.push({
|
||||
const onSecretDelete = useCallback(
|
||||
(index: number, secretName: string, id?: string, overrideId?: string) => {
|
||||
if (id)
|
||||
deletedSecretIds.current.push({
|
||||
id,
|
||||
secretName
|
||||
});
|
||||
if (overrideId) deletedSecretIds.current.push({
|
||||
if (overrideId)
|
||||
deletedSecretIds.current.push({
|
||||
id: overrideId,
|
||||
secretName
|
||||
});
|
||||
remove(index);
|
||||
// just the case if this is called from drawer
|
||||
handlePopUpClose("secretDetails");
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onCreateWsTag = useCallback(
|
||||
async (tagName: string, tagColor: string) => {
|
||||
async (tagName: string) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName,
|
||||
tagColor,
|
||||
tagSlug: tagName.replace(" ", "_")
|
||||
});
|
||||
handlePopUpClose("addTag");
|
||||
@@ -753,7 +760,9 @@ export const DashboardPage = () => {
|
||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||
<NavHeader
|
||||
pageName={t("dashboard.title")}
|
||||
currentEnv={userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""}
|
||||
currentEnv={
|
||||
userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""
|
||||
}
|
||||
isFolderMode
|
||||
folders={folderData?.dir}
|
||||
isProjectRelated
|
||||
@@ -816,9 +825,15 @@ export const DashboardPage = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SecretRollback}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="block xl:hidden">
|
||||
<Tooltip content="Point-in-time Recovery">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("secretSnapshots")}
|
||||
@@ -827,6 +842,13 @@ export const DashboardPage = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SecretRollback}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="hidden xl:block">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
@@ -840,16 +862,24 @@ export const DashboardPage = () => {
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
isLoading={isLoadingSnapshotCount}
|
||||
isDisabled={!canDoRollback}
|
||||
isDisabled={!canDoRollback || !isAllowed}
|
||||
className="h-10"
|
||||
>
|
||||
{snapshotCount} Commits
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isAllowed}
|
||||
onClick={() => {
|
||||
if (!(isReadOnly || isRollbackMode)) {
|
||||
if (secretContainer.current) {
|
||||
@@ -867,6 +897,8 @@ export const DashboardPage = () => {
|
||||
<FontAwesomeIcon icon={faPlus} className="px-2" />
|
||||
Add Secret
|
||||
</button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||
<div className="cursor-pointer rounded-r-md border border-mineshaft-500 bg-mineshaft-600 p-2 text-sm text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]">
|
||||
@@ -876,28 +908,42 @@ export const DashboardPage = () => {
|
||||
<DropdownMenuContent align="end" className="left-20 z-[60] mt-1 w-[10.8rem]">
|
||||
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<div className="w-full pb-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Folders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretImports}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Import
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
@@ -917,10 +963,25 @@ export const DashboardPage = () => {
|
||||
Go back
|
||||
</Button>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={
|
||||
isRollbackMode
|
||||
? ProjectPermissionActions.Create
|
||||
: (ProjectPermissionActions.Edit as any)
|
||||
}
|
||||
a={
|
||||
isRollbackMode
|
||||
? ProjectPermissionSub.SecretRollback
|
||||
: ProjectPermissionSub.Secrets
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={isSubmitDisabled}
|
||||
isDisabled={isSubmitDisabled || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||
leftIcon={
|
||||
<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />
|
||||
}
|
||||
onClick={handleSubmit(onSaveSecret)}
|
||||
className="h-10 text-black"
|
||||
color="primary"
|
||||
@@ -928,6 +989,8 @@ export const DashboardPage = () => {
|
||||
>
|
||||
{isRollbackMode ? "Rollback" : "Save Changes"}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -986,14 +1049,22 @@ export const DashboardPage = () => {
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -1154,4 +1225,6 @@ export const DashboardPage = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets }
|
||||
);
|
||||
|
||||
@@ -2,7 +2,9 @@ import { memo } from "react";
|
||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
type Props = {
|
||||
folders?: Array<{ id: string; name: string }>;
|
||||
@@ -47,12 +49,18 @@ export const FolderSection = memo(
|
||||
{name}
|
||||
</div>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Folders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Settings" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleFolderUpdate(id, name)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
@@ -60,6 +68,13 @@ export const FolderSection = memo(
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Folders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
@@ -67,12 +82,15 @@ export const FolderSection = memo(
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+18
-2
@@ -2,6 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
Switch,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||
@@ -85,12 +87,24 @@ export const SecretDetailDrawer = ({
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly}>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
isDisabled={isReadOnly}
|
||||
isDisabled={isReadOnly || !isAllowed}
|
||||
onClick={() => {
|
||||
const secret = getValues(`secrets.${index}`);
|
||||
|
||||
@@ -99,6 +113,8 @@ export const SecretDetailDrawer = ({
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
@@ -78,7 +80,8 @@ type Props = {
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export const SecretDropzone = ({
|
||||
export const SecretDropzone = withProjectPermission(
|
||||
({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
@@ -175,7 +178,7 @@ export const SecretDropzone = ({
|
||||
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setDragActive.off();
|
||||
parseFile(e.dataTransfer.files[0], e.dataTransfer?.files?.[0]?.type === "application/json");
|
||||
parseFile(e.dataTransfer.files[0]);
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -223,7 +226,12 @@ export const SecretDropzone = ({
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
@@ -361,7 +369,9 @@ export const SecretDropzone = ({
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
onCheckedChange={(isChecked) =>
|
||||
onChange(isChecked ? secVal : "")
|
||||
}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
@@ -407,4 +417,6 @@ export const SecretDropzone = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Create, subject: ProjectPermissionSub.Secrets }
|
||||
);
|
||||
|
||||
+32
-8
@@ -9,9 +9,10 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
onDelete: (environment: string, secretPath: string) => void;
|
||||
@@ -49,7 +50,9 @@ export const SecretImportItem = ({
|
||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||
);
|
||||
|
||||
if (filteredSecrets.length > 0 && searchTerm) {
|
||||
setIsExpanded.on();
|
||||
@@ -58,7 +61,6 @@ export const SecretImportItem = ({
|
||||
}
|
||||
}, [searchTerm]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setIsExpanded.off();
|
||||
@@ -78,7 +80,11 @@ export const SecretImportItem = ({
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<td className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${isExpanded && "border-t-2 border-mineshaft-500"}`}>
|
||||
<td
|
||||
className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${
|
||||
isExpanded && "border-t-2 border-mineshaft-500"
|
||||
}`}
|
||||
>
|
||||
<Tooltip content="Secret Import" className="capitalize">
|
||||
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
|
||||
</Tooltip>
|
||||
@@ -106,6 +112,11 @@ export const SecretImportItem = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SecretImports}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
@@ -113,6 +124,7 @@ export const SecretImportItem = ({
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
@@ -122,12 +134,17 @@ export const SecretImportItem = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{isExpanded && !isDragging && (
|
||||
<td colSpan={3} className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}>
|
||||
<td
|
||||
colSpan={3}
|
||||
className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
||||
>
|
||||
<div className="rounded-md bg-bunker-700 p-1">
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
@@ -146,7 +163,11 @@ export const SecretImportItem = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => (
|
||||
{importedSecrets
|
||||
.filter((secret) =>
|
||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||
)
|
||||
.map(({ key, value, overriden }, index) => (
|
||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
@@ -155,7 +176,10 @@ export const SecretImportItem = ({
|
||||
<SecretInput value={value} isDisabled isVisible />
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
<EnvFolderIcon
|
||||
env={overriden?.env}
|
||||
secretPath={overriden?.secretPath}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { cx } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@@ -34,6 +35,10 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
@@ -452,6 +457,11 @@ export const SecretInputRow = memo(
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
@@ -459,7 +469,7 @@ export const SecretInputRow = memo(
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
||||
onClick={() => {
|
||||
onSecretDelete(index, secKey, secId, idOverride);
|
||||
}}
|
||||
@@ -468,6 +478,8 @@ export const SecretInputRow = memo(
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
LogsSection
|
||||
} from "./components";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
export const AuditLogsPage = () => {
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
@@ -14,4 +16,6 @@ export const AuditLogsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { IPAllowlistSection } from "./components";
|
||||
|
||||
export const IPAllowlistPage = () => {
|
||||
export const IPAllowlistPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
@@ -12,4 +16,6 @@ export const IPAllowlistPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.IpAllowList }
|
||||
);
|
||||
|
||||
@@ -2,15 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteTrustedIp
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeleteTrustedIp } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IPAllowlistModal } from "./IPAllowlistModal";
|
||||
@@ -30,7 +30,6 @@ export const IPAllowlistSection = () => {
|
||||
|
||||
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
|
||||
try {
|
||||
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
await mutateAsync({
|
||||
@@ -51,28 +50,34 @@ export const IPAllowlistSection = () => {
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
IP Allowlist
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">IP Allowlist</h2>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("trustedIp")
|
||||
handlePopUpOpen("trustedIp");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
isLoading={false}
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add IP
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<IPAllowlistTable
|
||||
popUp={popUp}
|
||||
@@ -92,7 +97,9 @@ export const IPAllowlistSection = () => {
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteTrustedIpSubmit((popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId)
|
||||
onDeleteTrustedIpSubmit(
|
||||
(popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
@@ -102,4 +109,4 @@ export const IPAllowlistSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { faGlobe, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -14,10 +15,13 @@ import {
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetTrustedIps
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useGetTrustedIps } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -30,23 +34,21 @@ type Props = {
|
||||
comment?: string;
|
||||
isActive?: boolean;
|
||||
prefix?: number;
|
||||
},
|
||||
}
|
||||
) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const IPAllowlistTable = ({
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
export const IPAllowlistTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
|
||||
|
||||
const formatType = (type: string, prefix?: number) => {
|
||||
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${(prefix !== undefined) ? "CIDR" : ""}`;
|
||||
}
|
||||
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${
|
||||
prefix !== undefined ? "CIDR" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -62,30 +64,17 @@ export const IPAllowlistTable = ({
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && data && data?.length > 0 && data
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data
|
||||
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
|
||||
.map(({
|
||||
_id,
|
||||
ipAddress,
|
||||
comment,
|
||||
type,
|
||||
prefix,
|
||||
isActive
|
||||
}) => {
|
||||
.map(({ _id, ipAddress, comment, type, prefix, isActive }) => {
|
||||
return (
|
||||
<Tr
|
||||
key={`ip-access-range-${_id}`}
|
||||
className="h-10"
|
||||
>
|
||||
<Td>
|
||||
{`${ipAddress}${(prefix !== undefined) ? `/${prefix}` : ""}`}
|
||||
</Td>
|
||||
<Td>
|
||||
{formatType(type, prefix)}
|
||||
</Td>
|
||||
<Td>
|
||||
{comment}
|
||||
</Td>
|
||||
<Tr key={`ip-access-range-${_id}`} className="h-10">
|
||||
<Td>{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}</Td>
|
||||
<Td>{formatType(type, prefix)}</Td>
|
||||
<Td>{comment}</Td>
|
||||
{/* <Td>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
@@ -96,6 +85,11 @@ export const IPAllowlistTable = ({
|
||||
</div>
|
||||
</Td> */}
|
||||
<Td className="flex items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
@@ -114,9 +108,17 @@ export const IPAllowlistTable = ({
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
@@ -131,21 +133,23 @@ export const IPAllowlistTable = ({
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />}
|
||||
{isLoading && (
|
||||
<TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />
|
||||
)}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No IP addresses added"
|
||||
icon={faGlobe}
|
||||
/>
|
||||
<EmptyState title="No IP addresses added" icon={faGlobe} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -159,4 +163,4 @@ export const IPAllowlistTable = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ 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 { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
@@ -15,7 +16,8 @@ enum TabSections {
|
||||
Roles = "roles"
|
||||
}
|
||||
|
||||
export const MembersPage = () => {
|
||||
export const MembersPage = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
@@ -29,13 +31,13 @@ export const MembersPage = () => {
|
||||
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>
|
||||
<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
|
||||
@@ -49,10 +51,15 @@ export const MembersPage = () => {
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<ProjectRoleListTab roles={roles as TRole<string>[]} isRolesLoading={isRolesLoading} />
|
||||
<ProjectRoleListTab
|
||||
roles={roles as TRole<string>[]}
|
||||
isRolesLoading={isRolesLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Member }
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
import {
|
||||
GeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useUser,
|
||||
useWorkspace
|
||||
@@ -240,7 +242,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
@@ -250,7 +252,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -276,9 +278,9 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={GeneralPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<>
|
||||
@@ -316,13 +318,13 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?._id && (
|
||||
<OrgPermissionCan
|
||||
I={GeneralPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -336,7 +338,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
+7
-2
@@ -1,5 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
@@ -11,7 +13,8 @@ type Props = {
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
export const ProjectRoleListTab = withProjectPermission(
|
||||
({ roles = [], isRolesLoading }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
|
||||
return popUp.editRole.isOpen ? (
|
||||
@@ -42,4 +45,6 @@ export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
|
||||
);
|
||||
|
||||
+39
-4
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -19,7 +20,12 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRole } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
@@ -69,9 +75,17 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
placeholder="Search roles..."
|
||||
/>
|
||||
</div>
|
||||
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => onSelectRole()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -99,8 +113,15 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="Edit">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit"
|
||||
onClick={() => onSelectRole(role)}
|
||||
variant="plain"
|
||||
@@ -108,19 +129,33 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip
|
||||
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
|
||||
content={
|
||||
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||
variant="plain"
|
||||
isDisabled={isNonMutatable}
|
||||
isDisabled={isNonMutatable || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
+8
@@ -34,6 +34,7 @@ import {
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
import { SingleProjectPermission } from "./SingleProjectPermission";
|
||||
import { WsProjectPermission } from "./WsProjectPermission";
|
||||
|
||||
const SINGLE_PERMISSION_LIST = [
|
||||
{
|
||||
@@ -271,6 +272,13 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col space-y-4" key="permission-ws">
|
||||
<WsProjectPermission
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isNonEditable={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 mt-12">
|
||||
<Button
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
|
||||
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 = {
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "edit", label: "Update" },
|
||||
{ action: "delete", label: "Remove" }
|
||||
] as const;
|
||||
|
||||
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.workspace"
|
||||
});
|
||||
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;
|
||||
|
||||
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.workspace", { edit: false, delete: false }, { shouldDirty: true });
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
|
||||
break;
|
||||
default:
|
||||
setValue("permissions.workspace", { edit: 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={faPuzzlePiece} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="font-medium mb-1 text-lg">Workspace</div>
|
||||
<div className="text-xs font-light">Workspace control actions</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.workspace.${action}`}
|
||||
key={`permissions.workspace.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.workspace.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,7 +23,13 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useCreateSecretV3,
|
||||
useDeleteSecretV3,
|
||||
@@ -38,7 +44,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||
|
||||
export const SecretOverviewPage = () => {
|
||||
const SecretOverview = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const router = useRouter();
|
||||
@@ -394,3 +400,8 @@ export const SecretOverviewPage = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
|
||||
action: ProjectPermissionActions.Read,
|
||||
subject: ProjectPermissionSub.Secrets
|
||||
});
|
||||
|
||||
+18
-2
@@ -3,7 +3,9 @@ import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-i
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
@@ -91,19 +93,26 @@ export const SecretEditRow = ({
|
||||
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
|
||||
{isDirty ? (
|
||||
<>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="save">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="submit-value"
|
||||
className="h-full"
|
||||
isDisabled={isSubmitting}
|
||||
isDisabled={isSubmitting || !isAllowed}
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div>
|
||||
<Tooltip content="cancel">
|
||||
<IconButton
|
||||
@@ -132,6 +141,11 @@ export const SecretEditRow = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
@@ -139,12 +153,14 @@ export const SecretEditRow = ({
|
||||
ariaLabel="delete-value"
|
||||
className="h-full"
|
||||
onClick={handleDeleteSecret}
|
||||
isDisabled={isDeleting}
|
||||
isDisabled={isDeleting || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
||||
@@ -14,7 +16,8 @@ const tabs = [
|
||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||
];
|
||||
|
||||
export const ProjectSettingsPage = () => {
|
||||
export const ProjectSettingsPage = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||
@@ -32,7 +35,9 @@ export const ProjectSettingsPage = () => {
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
|
||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
@@ -55,4 +60,6 @@ export const ProjectSettingsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
+14
-4
@@ -1,11 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||
|
||||
export const AutoCapitalizationSection = () => {
|
||||
export const AutoCapitalizationSection = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -32,14 +35,17 @@ export const AutoCapitalizationSection = () => {
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleCapitalizationToggle(state as boolean);
|
||||
@@ -47,6 +53,10 @@ export const AutoCapitalizationSection = () => {
|
||||
>
|
||||
{t("settings.project.auto-capitalization-description")}
|
||||
</Checkbox>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
+15
-7
@@ -3,19 +3,23 @@ import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useDeleteWorkspace
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useDeleteWorkspace } from "@app/hooks/api";
|
||||
|
||||
export const DeleteProjectSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization()
|
||||
const { currentOrg } = useOrganization();
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState("");
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
@@ -66,17 +70,21 @@ export const DeleteProjectSection = () => {
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={onDeleteWorkspace}
|
||||
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
{t("settings.project.delete-project")}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t("settings.project.delete-project-note")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+22
-6
@@ -1,12 +1,15 @@
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
|
||||
export const E2EESection = () => {
|
||||
export const E2EESection = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||
@@ -78,22 +81,35 @@ export const E2EESection = () => {
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
||||
read/decrypt your secret values.
|
||||
</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
isDisabled={!isAllowed}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
) : <div />;
|
||||
};
|
||||
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
},
|
||||
{
|
||||
action: ProjectPermissionActions.Read,
|
||||
subject: ProjectPermissionSub.Settings
|
||||
}
|
||||
);
|
||||
|
||||
+30
-18
@@ -2,29 +2,34 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDeleteWsEnvironment } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddEnvironmentModal } from "./AddEnvironmentModal";
|
||||
import { EnvironmentTable } from "./EnvironmentTable";
|
||||
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
|
||||
|
||||
export const EnvironmentSection = () => {
|
||||
export const EnvironmentSection = withProjectPermission(
|
||||
() => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const deleteWsEnvironment = useDeleteWsEnvironment();
|
||||
|
||||
const isMoreEnvironmentsAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
|
||||
const isMoreEnvironmentsAllowed =
|
||||
subscription?.environmentLimit && currentWorkspace?.environments
|
||||
? currentWorkspace.environments.length < subscription.environmentLimit
|
||||
: true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"createEnv",
|
||||
@@ -60,10 +65,13 @@ export const EnvironmentSection = () => {
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Environments
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
@@ -74,17 +82,19 @@ export const EnvironmentSection = () => {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create environment
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Choose which environments will show up in your dashboard like development, staging, production
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<EnvironmentTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
@@ -113,4 +123,6 @@ export const EnvironmentSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
|
||||
);
|
||||
|
||||
+16
-1
@@ -2,6 +2,7 @@ import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-soli
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useReorderWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
@@ -115,17 +116,28 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</IconButton>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteEnv", { name, slug });
|
||||
@@ -134,9 +146,12 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+19
-13
@@ -3,7 +3,8 @@ import {
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceIndexStatus,
|
||||
@@ -11,9 +12,15 @@ import {
|
||||
useNameWorkspaceSecrets
|
||||
} from "@app/hooks/api";
|
||||
|
||||
export const ProjectIndexSecretsSection = () => {
|
||||
// TODO: add check so that this only shows up if user is
|
||||
// an admin in the workspace
|
||||
|
||||
export const ProjectIndexSecretsSection = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(currentWorkspace?._id ?? "");
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
||||
currentWorkspace?._id ?? ""
|
||||
);
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||
@@ -50,22 +57,21 @@ export const ProjectIndexSecretsSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
return (!isBlindIndexedLoading && (isBlindIndexed === false)) ? (
|
||||
return !isBlindIndexedLoading && !isBlindIndexed ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
|
||||
Your project, created before the introduction of blind indexing, contains unindexed
|
||||
secrets. To access individual secrets by name through the SDK and public API, please
|
||||
enable blind indexing.
|
||||
</p>
|
||||
<Button
|
||||
onClick={onEnableBlindIndices}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
>
|
||||
<Button onClick={onEnableBlindIndices} color="mineshaft" size="sm" type="submit">
|
||||
Enable Blind Indexing
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
+11
-20
@@ -4,11 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useRenameWorkspace
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useRenameWorkspace } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name")
|
||||
@@ -21,11 +20,7 @@ export const ProjectNameChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
@@ -33,7 +28,6 @@ export const ProjectNameChangeSection = () => {
|
||||
name: currentWorkspace.name
|
||||
});
|
||||
}
|
||||
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
@@ -49,7 +43,6 @@ export const ProjectNameChangeSection = () => {
|
||||
text: "Successfully renamed workspace",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
@@ -64,33 +57,31 @@ export const ProjectNameChangeSection = () => {
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
Project Name
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
<Input placeholder="Project name" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-11
@@ -2,10 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteWsTag } from "@app/hooks/api";
|
||||
|
||||
@@ -14,7 +14,8 @@ import { SecretTagsTable } from "./SecretTagsTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const SecretTagsSection = (): JSX.Element => {
|
||||
export const SecretTagsSection = withProjectPermission(
|
||||
(): JSX.Element => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"CreateSecretTag",
|
||||
@@ -48,6 +49,8 @@ export const SecretTagsSection = (): JSX.Element => {
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
@@ -56,17 +59,18 @@ export const SecretTagsSection = (): JSX.Element => {
|
||||
handlePopUpOpen("CreateSecretTag");
|
||||
console.log("x2");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create tag
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for
|
||||
the current project.
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
|
||||
current project.
|
||||
</p>
|
||||
<SecretTagsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddSecretTagModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
@@ -84,4 +88,6 @@ export const SecretTagsSection = (): JSX.Element => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
|
||||
);
|
||||
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetWsTags } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -53,6 +54,11 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
@@ -62,9 +68,12 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+16
-2
@@ -3,7 +3,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteServiceToken } from "@app/hooks/api";
|
||||
|
||||
@@ -12,7 +15,8 @@ import { ServiceTokenTable } from "./ServiceTokenTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ServiceTokenSection = () => {
|
||||
export const ServiceTokenSection = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
@@ -48,15 +52,23 @@ export const ServiceTokenSection = () => {
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
{t("section.token.service-tokens")}
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||
@@ -73,4 +85,6 @@ export const ServiceTokenSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
);
|
||||
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -70,6 +71,11 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
@@ -79,9 +85,12 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+46
-5
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateWebhook,
|
||||
@@ -29,7 +31,8 @@ import {
|
||||
|
||||
import { AddWebhookForm, TFormSchema } from "./AddWebhookForm";
|
||||
|
||||
export const WebhooksTab = () => {
|
||||
export const WebhooksTab = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -139,12 +142,20 @@ export const WebhooksTab = () => {
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addWebhook")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
|
||||
<div>
|
||||
@@ -219,23 +230,41 @@ export const WebhooksTab = () => {
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="star"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookTest(id)}
|
||||
isDisabled={
|
||||
(isTestWebhookSubmitting &&
|
||||
testWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
|
||||
}
|
||||
isLoading={isTestWebhookSubmitting && testWebhookVars?.webhookId === id}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookDisable(id, !isDisabled)}
|
||||
isDisabled={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
(isUpdateWebhookSubmitting &&
|
||||
updateWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
@@ -243,15 +272,25 @@ export const WebhooksTab = () => {
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("deleteWebhook", id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -277,4 +316,6 @@ export const WebhooksTab = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Webhooks }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user