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