mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(rbac): added new permission check for workspace in frontend
This commit is contained in:
@@ -4,7 +4,7 @@ import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import Role from "../../models/role";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
@@ -107,10 +109,6 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
|
||||
|
||||
if (![ADMIN, MEMBER].includes(role)) {
|
||||
throw new Error("Failed to validate role");
|
||||
}
|
||||
|
||||
// validate target membership
|
||||
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
|
||||
"user"
|
||||
@@ -129,9 +127,32 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const oldRole = membershipToChangeRole.role;
|
||||
membershipToChangeRole.role = role;
|
||||
await membershipToChangeRole.save();
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
if (isCustomRole) {
|
||||
const wsRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
if (!wsRole) throw BadRequestError({ message: "Role not found" });
|
||||
const membership = await Membership.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: wsRole
|
||||
});
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
@@ -140,8 +161,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
userId: membershipToChangeRole.user._id.toString(),
|
||||
email: membershipToChangeRole.user.email,
|
||||
oldRole,
|
||||
newRole: membershipToChangeRole.role
|
||||
oldRole: membershipToChangeRole.role,
|
||||
newRole: role
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -150,7 +171,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership: membershipToChangeRole
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
userHasWriteOnlyAbility
|
||||
} from "../../ee/helpers/checkMembershipPermissions";
|
||||
import _ from "lodash";
|
||||
import { BatchSecret } from "../../types/secret";
|
||||
import {
|
||||
getFolderByPath,
|
||||
getFolderIdFromServiceToken,
|
||||
@@ -56,8 +55,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
body: { secretPath, folderId }
|
||||
} = validatedData;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
const createSecrets: any[] = [];
|
||||
const updateSecrets: any[] = [];
|
||||
const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = [];
|
||||
const actions: IAction[] = [];
|
||||
|
||||
@@ -111,7 +110,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
version: 1,
|
||||
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
workspace: workspaceId,
|
||||
folder: folderId,
|
||||
secretBlindIndex,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
@@ -126,7 +125,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
updateSecrets.push({
|
||||
...request.secret,
|
||||
_id: new Types.ObjectId(request.secret._id),
|
||||
_id: request.secret._id,
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
@@ -145,7 +144,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
// handle create secrets
|
||||
let createdSecrets: ISecret[] = [];
|
||||
if (createSecrets.length > 0) {
|
||||
createdSecrets = await Secret.insertMany(createSecrets);
|
||||
createdSecrets = (await Secret.insertMany(createSecrets)) as any;
|
||||
// (EE) add secret versions for new secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: createdSecrets.map((n: any) => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
} from "../models";
|
||||
import { IUser, User } from "../models";
|
||||
import { sendMail } from "./nodemailer";
|
||||
|
||||
/**
|
||||
@@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer";
|
||||
*/
|
||||
export const setupAccount = async ({ email }: { email: string }) => {
|
||||
const user = await new User({
|
||||
email,
|
||||
email
|
||||
}).save();
|
||||
|
||||
return user;
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => {
|
||||
* @returns {Object} user - the completed user
|
||||
*/
|
||||
export const completeAccount = async ({
|
||||
userId,
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
userId,
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}: {
|
||||
userId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
encryptionVersion: number;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
userId: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
encryptionVersion: number;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}) => {
|
||||
const options = {
|
||||
new: true,
|
||||
new: true
|
||||
};
|
||||
const user = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
@@ -82,12 +79,12 @@ export const completeAccount = async ({
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
verifier
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return user;
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,38 +95,42 @@ export const completeAccount = async ({
|
||||
* @param {String} obj.userAgent - login user-agent
|
||||
*/
|
||||
export const checkUserDevice = async ({
|
||||
user,
|
||||
ip,
|
||||
userAgent,
|
||||
user,
|
||||
ip,
|
||||
userAgent
|
||||
}: {
|
||||
user: IUser;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
user: IUser;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
}) => {
|
||||
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
||||
|
||||
if (!isDeviceSeen) {
|
||||
// case: unseen login ip detected for user
|
||||
// -> notify user about the sign-in from new ip
|
||||
|
||||
user.devices = user.devices.concat([{
|
||||
ip: String(ip),
|
||||
userAgent,
|
||||
}]);
|
||||
|
||||
await user.save();
|
||||
const isDeviceSeen = user.devices.some(
|
||||
(device) => device.ip === ip && device.userAgent === userAgent
|
||||
);
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "newDevice.handlebars",
|
||||
subjectLine: "Successful login from new device",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
email: user.email,
|
||||
timestamp: new Date().toString(),
|
||||
ip,
|
||||
userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!isDeviceSeen) {
|
||||
// case: unseen login ip detected for user
|
||||
// -> notify user about the sign-in from new ip
|
||||
|
||||
user.devices = user.devices.concat([
|
||||
{
|
||||
ip: String(ip),
|
||||
userAgent
|
||||
}
|
||||
]);
|
||||
|
||||
await user.save();
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "newDevice.handlebars",
|
||||
subjectLine: "Successful login from new device",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
email: user.email,
|
||||
timestamp: new Date().toString(),
|
||||
ip,
|
||||
userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { body, param } from "express-validator";
|
||||
import { requireAuth, validateRequest } from "../../middleware";
|
||||
import { requireAuth } from "../../middleware";
|
||||
import { membershipController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
@@ -14,8 +13,6 @@ router.get(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
param("workspaceId").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.validateMembership
|
||||
);
|
||||
|
||||
@@ -25,8 +22,6 @@ router.delete(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
param("membershipId").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.deleteMembership
|
||||
);
|
||||
|
||||
@@ -36,8 +31,6 @@ router.post(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
body("role").exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.changeMembershipRole
|
||||
);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export enum ProjectPermissionSub {
|
||||
Folders = "folders"
|
||||
}
|
||||
|
||||
type GenericFields = {
|
||||
type SubjectFields = {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
};
|
||||
@@ -42,17 +42,17 @@ type GenericFields = {
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & GenericFields)
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & GenericFields)
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & GenericFields)
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
|
||||
10
backend/src/types/secret/index.d.ts
vendored
10
backend/src/types/secret/index.d.ts
vendored
@@ -28,7 +28,13 @@ export interface BatchSecretRequest {
|
||||
}
|
||||
|
||||
export interface BatchSecret {
|
||||
_id: string;
|
||||
version?: number;
|
||||
_id?: string;
|
||||
user?: string;
|
||||
environment: string;
|
||||
workspace?: string;
|
||||
algorithm?: string;
|
||||
keyEncoding?: string;
|
||||
type: "shared" | "personal";
|
||||
secretName: string;
|
||||
secretBlindIndex: string;
|
||||
@@ -42,5 +48,5 @@ export interface BatchSecret {
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[];
|
||||
folder: string
|
||||
folder: string;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export const CompletedAccountSignupV3 = z.object({
|
||||
body: z.object({
|
||||
email: z.string().email().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional().nullish(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
|
||||
@@ -115,8 +115,8 @@ export const GetSecretVersionsV1 = z.object({
|
||||
secretId: z.string().trim()
|
||||
}),
|
||||
query: z.object({
|
||||
offset: z.number(),
|
||||
limit: z.number()
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -175,17 +175,19 @@ const batchUpdateRequestV2 = z.object({
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
tags: z.object({
|
||||
_id: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
slug: z.string().trim()
|
||||
})
|
||||
tags: z
|
||||
.object({
|
||||
_id: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
slug: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export const BatchSecretsV2 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
folderId: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().optional(),
|
||||
requests: z
|
||||
|
||||
@@ -130,8 +130,8 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
|
||||
query: z.object({
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
offset: z.number(),
|
||||
limit: z.number()
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -161,8 +161,8 @@ export const GetWorkspaceLogsV1 = z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
query: z.object({
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number(),
|
||||
sortBy: z.string().trim().optional(),
|
||||
userId: z.string().trim().optional(),
|
||||
actionNames: z.string().trim().optional()
|
||||
@@ -178,8 +178,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
|
||||
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
|
||||
startDate: z.string().datetime().nullable().optional(),
|
||||
endDate: z.string().datetime().nullable().optional(),
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number(),
|
||||
actor: z.string().nullish().optional()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -16,6 +16,15 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
|
||||
|
||||
console.log(workspaceId);
|
||||
if (!permission && currentWorkspace) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
Failed to load user permissions
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && workspaceId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
@@ -29,20 +38,8 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission && currentWorkspace) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
Failed to load user permissions
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission) {
|
||||
return <>children</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionContext.Provider value={permission}>
|
||||
<ProjectPermissionContext.Provider value={permission!}>
|
||||
{children}
|
||||
</ProjectPermissionContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
|
||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||
export { ProjectGeneralPermissionActions, ProjectPermissionSubjects } from "./types";
|
||||
export { ProjectPermissionActions,ProjectPermissionSub } from "./types";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { MongoAbility } from "@casl/ability";
|
||||
import { ForcedSubject, MongoAbility } from "@casl/ability";
|
||||
|
||||
export enum ProjectGeneralPermissionActions {
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSubjects {
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
Settings = "settings",
|
||||
@@ -21,24 +21,44 @@ export enum ProjectPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretImports = "secret-imports",
|
||||
SecretRollback = "secret-rollback",
|
||||
Folders = "folders"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Secrets]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Folders]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.SecretImports]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Role]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Tags]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Member]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Integrations]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Webhooks]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.AuditLogs]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Environments]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.IpAllowList]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Settings]
|
||||
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.ServiceTokens]
|
||||
| [ProjectGeneralPermissionActions.Delete, ProjectPermissionSubjects.Workspace]
|
||||
| [ProjectGeneralPermissionActions.Edit, ProjectPermissionSubjects.Workspace];
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -7,10 +7,11 @@ export {
|
||||
OrgPermissionSubjects,
|
||||
useOrgPermission
|
||||
} from "./OrgPermissionContext";
|
||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||
export {
|
||||
ProjectGeneralPermissionActions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionProvider,
|
||||
ProjectPermissionSubjects,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "./ProjectPermissionContext";
|
||||
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { withPermission } from "./withPermission";
|
||||
export { withProjectPermission } from "./withProjectPermission";
|
||||
|
||||
1
frontend/src/hoc/withProjectPermission/index.tsx
Normal file
1
frontend/src/hoc/withProjectPermission/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { withProjectPermission } from "./withProjectPermission";
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ComponentType } from "react";
|
||||
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { TProjectPermission, useProjectPermission } from "@app/context";
|
||||
|
||||
type Props<T extends Abilities> = (T extends AbilityTuple
|
||||
? {
|
||||
action: T[0];
|
||||
subject: Extract<T[1], SubjectType>;
|
||||
}
|
||||
: {
|
||||
action: string;
|
||||
subject: string;
|
||||
}) & { className?: string; containerClassName?: string };
|
||||
|
||||
export const withProjectPermission = <T extends {}, J extends TProjectPermission>(
|
||||
Component: ComponentType<T>,
|
||||
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
|
||||
) => {
|
||||
const HOC = (hocProps: T) => {
|
||||
const permission = useProjectPermission();
|
||||
|
||||
// akhilmhdh: Set as any due to casl/react ts type bug
|
||||
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
|
||||
if (permission.cannot(action as any, subject)) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"container h-full mx-auto flex justify-center items-center",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-medium mb-2">Permission Denied</div>
|
||||
<div className="text-sm">
|
||||
You do not have permission to this page. <br /> Kindly contact your organization
|
||||
administrator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component {...hocProps} />;
|
||||
};
|
||||
|
||||
HOC.displayName = "WithProjectPermission";
|
||||
return HOC;
|
||||
};
|
||||
@@ -6,8 +6,9 @@ import { useRouter } from "next/router";
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import EventFilter from "@app/components/basic/EventFilter";
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
|
||||
@@ -23,10 +24,10 @@ interface LogData {
|
||||
};
|
||||
serviceAccount?: {
|
||||
string: string;
|
||||
},
|
||||
};
|
||||
serviceTokenData?: {
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
@@ -60,67 +61,33 @@ interface LogDataPoint {
|
||||
/**
|
||||
* This is the tab that includes all of the user activity logs
|
||||
*/
|
||||
export default function Activity() {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState("");
|
||||
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>();
|
||||
const { t } = useTranslation();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const Activity = withProjectPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState("");
|
||||
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>();
|
||||
const { t } = useTranslation();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
setCurrentOffset(0);
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: 0,
|
||||
limit: currentLimit,
|
||||
userId: "",
|
||||
actionNames: eventChosen
|
||||
});
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
setCurrentOffset(0);
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: 0,
|
||||
limit: currentLimit,
|
||||
userId: "",
|
||||
actionNames: eventChosen
|
||||
});
|
||||
|
||||
setLogsData(
|
||||
tempLogsData.map((log: LogData) => ({
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log?.user?.email,
|
||||
serviceAccount: log?.serviceAccount,
|
||||
serviceTokenData: log?.serviceTokenData,
|
||||
payload: log.actions.map((action) => ({
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}))
|
||||
}))
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
// this use effect adds more data in case 'View More' button is clicked
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: currentOffset,
|
||||
limit: currentLimit,
|
||||
userId: "",
|
||||
actionNames: eventChosen
|
||||
});
|
||||
setLogsData(
|
||||
logsData.concat(
|
||||
setLogsData(
|
||||
tempLogsData.map((log: LogData) => ({
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
@@ -135,63 +102,103 @@ export default function Activity() {
|
||||
secretVersions: action.payload.secretVersions
|
||||
}))
|
||||
}))
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
// this use effect adds more data in case 'View More' button is clicked
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: currentOffset,
|
||||
limit: currentLimit,
|
||||
userId: "",
|
||||
actionNames: eventChosen
|
||||
});
|
||||
setLogsData(
|
||||
logsData.concat(
|
||||
tempLogsData.map((log: LogData) => ({
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log?.user?.email,
|
||||
serviceAccount: log?.serviceAccount,
|
||||
serviceTokenData: log?.serviceTokenData,
|
||||
payload: log.actions.map((action) => ({
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}))
|
||||
}))
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
getLogData();
|
||||
}, [currentLimit, currentOffset]);
|
||||
|
||||
const loadMoreLogs = () => {
|
||||
if (subscription?.auditLogs === false) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
} else {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [currentLimit, currentOffset]);
|
||||
|
||||
const loadMoreLogs = () => {
|
||||
if (subscription?.auditLogs === false) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
} else {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full h-full max-w-7xl">
|
||||
<Head>
|
||||
<title>Audit Logs</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
{currentSidebarAction && (
|
||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||
)}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl mt-6">
|
||||
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
|
||||
return (
|
||||
<div className="mx-auto w-full h-full max-w-7xl">
|
||||
<Head>
|
||||
<title>Audit Logs</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
{currentSidebarAction && (
|
||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||
)}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl mt-6">
|
||||
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>
|
||||
</div>
|
||||
<div className="px-6 h-8 mt-2">
|
||||
<EventFilter selected={eventChosen} select={setEventChosen} />
|
||||
</div>
|
||||
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
|
||||
<div className="flex justify-center w-full mb-6">
|
||||
<div className="items-center w-60">
|
||||
<Button
|
||||
text={String(t("common.view-more"))}
|
||||
textDisabled={String(t("common.end-of-history"))}
|
||||
active={logsData.length % 10 === 0}
|
||||
onButtonPressed={loadMoreLogs}
|
||||
size="md"
|
||||
color="mineshaft"
|
||||
<div className="px-6 h-8 mt-2">
|
||||
<EventFilter selected={eventChosen} select={setEventChosen} />
|
||||
</div>
|
||||
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
|
||||
<div className="flex justify-center w-full mb-6">
|
||||
<div className="items-center w-60">
|
||||
<Button
|
||||
text={String(t("common.view-more"))}
|
||||
textDisabled={String(t("common.end-of-history"))}
|
||||
active={logsData.length % 10 === 0}
|
||||
onButtonPressed={loadMoreLogs}
|
||||
size="md"
|
||||
color="mineshaft"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && (
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
text={
|
||||
subscription.slug === null
|
||||
? "You can see more logs under an Enterprise license"
|
||||
: "You can see more logs if you switch to Infisical's Business/Professional Plan."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{subscription && (
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
|
||||
Activity.requireAuth = true;
|
||||
Object.assign(Activity, { requireAuth: true });
|
||||
|
||||
export default Activity;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@ import { memo } from "react";
|
||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
type Props = {
|
||||
folders?: Array<{ id: string; name: string }>;
|
||||
@@ -47,32 +49,48 @@ export const FolderSection = memo(
|
||||
{name}
|
||||
</div>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Settings" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
onClick={() => handleFolderUpdate(id, name)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Folders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Settings" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleFolderUpdate(id, name)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Folders}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
Switch,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||
@@ -85,20 +87,34 @@ export const SecretDetailDrawer = ({
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly}>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
isDisabled={isReadOnly}
|
||||
onClick={() => {
|
||||
const secret = getValues(`secrets.${index}`);
|
||||
|
||||
onSecretDelete(index, secret.key, secret._id, secret.idOverride);
|
||||
}}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{(isAllowed) => (
|
||||
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
isDisabled={isReadOnly || !isAllowed}
|
||||
onClick={() => {
|
||||
const secret = getValues(`secrets.${index}`);
|
||||
|
||||
onSecretDelete(index, secret.key, secret._id, secret.idOverride);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
@@ -78,333 +80,343 @@ type Props = {
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export const SecretDropzone = ({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
environments = [],
|
||||
workspaceId,
|
||||
decryptFileKey
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragActive, setDragActive] = useToggle();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||
});
|
||||
|
||||
const secretPath = watch("secretPath");
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const debouncedSecretPath = useDebounce(secretPath);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
export const SecretDropzone = withProjectPermission(
|
||||
({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
environments = [],
|
||||
workspaceId,
|
||||
env: selectedEnvSlug,
|
||||
secretPath: debouncedSecretPath,
|
||||
isPaused:
|
||||
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
|
||||
!popUp.importSecEnv.isOpen,
|
||||
decryptFileKey
|
||||
});
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragActive, setDragActive] = useToggle();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", {});
|
||||
setSearchFilter("");
|
||||
}, [debouncedSecretPath]);
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||
});
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === "dragenter" || e.type === "dragover") {
|
||||
setDragActive.on();
|
||||
} else if (e.type === "dragleave") {
|
||||
setDragActive.off();
|
||||
}
|
||||
};
|
||||
const secretPath = watch("secretPath");
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const debouncedSecretPath = useDebounce(secretPath);
|
||||
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
const reader = new FileReader();
|
||||
if (!file) {
|
||||
createNotification({
|
||||
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
||||
type: "error",
|
||||
timeoutMs: 10000
|
||||
});
|
||||
return;
|
||||
}
|
||||
// const fileType = file.name.split('.')[1];
|
||||
setIsLoading.on();
|
||||
reader.onload = (event) => {
|
||||
if (!event?.target?.result) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
const env = isJson
|
||||
? parseJson(event.target.result as ArrayBuffer)
|
||||
: parseDotEnv(event.target.result as ArrayBuffer);
|
||||
setIsLoading.off();
|
||||
onParsedEnv(env);
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnvSlug,
|
||||
secretPath: debouncedSecretPath,
|
||||
isPaused:
|
||||
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
|
||||
!popUp.importSecEnv.isOpen,
|
||||
decryptFileKey
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", {});
|
||||
setSearchFilter("");
|
||||
}, [debouncedSecretPath]);
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === "dragenter" || e.type === "dragover") {
|
||||
setDragActive.on();
|
||||
} else if (e.type === "dragleave") {
|
||||
setDragActive.off();
|
||||
}
|
||||
};
|
||||
|
||||
// If something is wrong show an error
|
||||
try {
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setDragActive.off();
|
||||
parseFile(e.dataTransfer.files[0], e.dataTransfer?.files?.[0]?.type === "application/json");
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
||||
};
|
||||
|
||||
const handleFormSubmit = (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(data.secrets || {}).forEach((key) => {
|
||||
if (data.secrets[key]) {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||
comments: [""]
|
||||
};
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
const reader = new FileReader();
|
||||
if (!file) {
|
||||
createNotification({
|
||||
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
||||
type: "error",
|
||||
timeoutMs: 10000
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
handlePopUpClose("importSecEnv");
|
||||
reset();
|
||||
};
|
||||
// const fileType = file.name.split('.')[1];
|
||||
setIsLoading.on();
|
||||
reader.onload = (event) => {
|
||||
if (!event?.target?.result) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
const env = isJson
|
||||
? parseJson(event.target.result as ArrayBuffer)
|
||||
: parseDotEnv(event.target.result as ArrayBuffer);
|
||||
setIsLoading.off();
|
||||
onParsedEnv(env);
|
||||
};
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets?.secrets) {
|
||||
setValue(
|
||||
"secrets",
|
||||
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
// If something is wrong show an error
|
||||
try {
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
isDragActive && "opacity-100",
|
||||
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
||||
isLoading && "bg-bunker-800"
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center justify-cente flex-col space-y-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml,.json"
|
||||
onChange={handleFileUpload}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setDragActive.off();
|
||||
parseFile(e.dataTransfer.files[0]);
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
||||
};
|
||||
|
||||
const handleFormSubmit = (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(data.secrets || {}).forEach((key) => {
|
||||
if (data.secrets[key]) {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||
comments: [""]
|
||||
};
|
||||
}
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
handlePopUpClose("importSecEnv");
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets?.secrets) {
|
||||
setValue(
|
||||
"secrets",
|
||||
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
isDragActive && "opacity-100",
|
||||
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
||||
isLoading && "bg-bunker-800"
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center justify-center py-4",
|
||||
isSmaller && "py-1"
|
||||
)}
|
||||
>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<Modal
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("importSecEnv", isOpen);
|
||||
reset();
|
||||
setSearchFilter("");
|
||||
}}
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center justify-cente flex-col space-y-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml,.json"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center justify-center py-4",
|
||||
isSmaller && "py-1"
|
||||
)}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
Copy Secrets From An Environment
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Copy Secret From An Environment"
|
||||
subTitle="Copy/paste secrets from other environments into this context"
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<Modal
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("importSecEnv", isOpen);
|
||||
reset();
|
||||
setSearchFilter("");
|
||||
}}
|
||||
>
|
||||
<form>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<Input
|
||||
{...register("secretPath")}
|
||||
placeholder="Provide a path, default is /"
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
Copy Secrets From An Environment
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Copy Secret From An Environment"
|
||||
subTitle="Copy/paste secrets from other environments into this context"
|
||||
>
|
||||
<form>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>Secrets</div>
|
||||
<div className="w-1/2 flex items-center space-x-2">
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<Input
|
||||
placeholder="Search for secret"
|
||||
value={searchFilter}
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||
{...register("secretPath")}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>Secrets</div>
|
||||
<div className="w-1/2 flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Search for secret"
|
||||
value={searchFilter}
|
||||
size="xs"
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unselect All">
|
||||
<IconButton
|
||||
ariaLabel="UnSelect all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unselect All">
|
||||
<IconButton
|
||||
ariaLabel="UnSelect all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{!isSecretsLoading && !secrets?.secrets?.length && (
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
||||
{isSecretsLoading &&
|
||||
Array.apply(0, Array(2)).map((_x, i) => (
|
||||
<Skeleton
|
||||
key={`secret-pull-loading-${i + 1}`}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
))}
|
||||
|
||||
{secrets?.secrets
|
||||
?.filter(({ key }) =>
|
||||
key.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
?.map(({ _id, key, value: secVal }) => (
|
||||
<Controller
|
||||
key={`pull-secret--${_id}`}
|
||||
control={control}
|
||||
name={`secrets.${key}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) =>
|
||||
onChange(isChecked ? secVal : "")
|
||||
}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 mb-4">
|
||||
<Checkbox
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setShouldIncludeValues(isChecked as boolean)
|
||||
}
|
||||
>
|
||||
Include secret values
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!isSecretsLoading && !secrets?.secrets?.length && (
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
||||
{isSecretsLoading &&
|
||||
Array.apply(0, Array(2)).map((_x, i) => (
|
||||
<Skeleton
|
||||
key={`secret-pull-loading-${i + 1}`}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
))}
|
||||
|
||||
{secrets?.secrets
|
||||
?.filter(({ key }) =>
|
||||
key.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
?.map(({ _id, key, value: secVal }) => (
|
||||
<Controller
|
||||
key={`pull-secret--${_id}`}
|
||||
control={control}
|
||||
name={`secrets.${key}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 mb-4">
|
||||
<Checkbox
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setShouldIncludeValues(isChecked as boolean)
|
||||
}
|
||||
>
|
||||
Include secret values
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isSmaller && (
|
||||
<Button variant="star" onClick={onAddNewSecret}>
|
||||
Add a new secret
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isSmaller && (
|
||||
<Button variant="star" onClick={onAddNewSecret}>
|
||||
Add a new secret
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Create, subject: ProjectPermissionSub.Secrets }
|
||||
);
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
onDelete: (environment: string, secretPath: string) => void;
|
||||
@@ -49,7 +50,9 @@ export const SecretImportItem = ({
|
||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||
);
|
||||
|
||||
if (filteredSecrets.length > 0 && searchTerm) {
|
||||
setIsExpanded.on();
|
||||
@@ -58,7 +61,6 @@ export const SecretImportItem = ({
|
||||
}
|
||||
}, [searchTerm]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setIsExpanded.off();
|
||||
@@ -78,7 +80,11 @@ export const SecretImportItem = ({
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<td className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${isExpanded && "border-t-2 border-mineshaft-500"}`}>
|
||||
<td
|
||||
className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${
|
||||
isExpanded && "border-t-2 border-mineshaft-500"
|
||||
}`}
|
||||
>
|
||||
<Tooltip content="Secret Import" className="capitalize">
|
||||
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
|
||||
</Tooltip>
|
||||
@@ -106,28 +112,39 @@ export const SecretImportItem = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SecretImports}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{isExpanded && !isDragging && (
|
||||
<td colSpan={3} className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}>
|
||||
<td
|
||||
colSpan={3}
|
||||
className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
|
||||
>
|
||||
<div className="rounded-md bg-bunker-700 p-1">
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
@@ -146,19 +163,26 @@ export const SecretImportItem = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => (
|
||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isDisabled isVisible />
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{importedSecrets
|
||||
.filter((secret) =>
|
||||
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
|
||||
)
|
||||
.map(({ key, value, overriden }, index) => (
|
||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isDisabled isVisible />
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon
|
||||
env={overriden?.env}
|
||||
secretPath={overriden?.secretPath}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { cx } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@@ -34,6 +35,10 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
@@ -452,22 +457,29 @@ export const SecretInputRow = memo(
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="lg"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
onClick={() => {
|
||||
onSecretDelete(index, secKey, secId, idOverride);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="lg"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
||||
onClick={() => {
|
||||
onSecretDelete(index, secKey, secId, idOverride);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import {
|
||||
LogsSection
|
||||
} from "./components";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
export const AuditLogsPage = () => {
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection />
|
||||
</div>
|
||||
</div>
|
||||
<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="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { IPAllowlistSection } from "./components";
|
||||
|
||||
export const IPAllowlistPage = () => {
|
||||
export const IPAllowlistPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">IP Allowlist</p>
|
||||
<div />
|
||||
</div>
|
||||
<IPAllowlistSection />
|
||||
</div>
|
||||
</div>
|
||||
<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="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">IP Allowlist</p>
|
||||
<div />
|
||||
</div>
|
||||
<IPAllowlistSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.IpAllowList }
|
||||
);
|
||||
|
||||
@@ -2,104 +2,111 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
useDeleteTrustedIp
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeleteTrustedIp } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IPAllowlistModal } from "./IPAllowlistModal";
|
||||
import { IPAllowlistTable } from "./IPAllowlistTable";
|
||||
|
||||
export const IPAllowlistSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { mutateAsync } = useDeleteTrustedIp();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"trustedIp",
|
||||
"deleteTrustedIp",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
|
||||
try {
|
||||
|
||||
if (!currentWorkspace?._id) return;
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { mutateAsync } = useDeleteTrustedIp();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
await mutateAsync({
|
||||
workspaceId: currentWorkspace._id,
|
||||
trustedIpId
|
||||
});
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"trustedIp",
|
||||
"deleteTrustedIp",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted IP access range",
|
||||
type: "success"
|
||||
});
|
||||
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
handlePopUpClose("deleteTrustedIp");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
text: "Failed to delete IP access range",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
await mutateAsync({
|
||||
workspaceId: currentWorkspace._id,
|
||||
trustedIpId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted IP access range",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTrustedIp");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
text: "Failed to delete IP access range",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
IP Allowlist
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("trustedIp")
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
isLoading={false}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add IP
|
||||
</Button>
|
||||
</div>
|
||||
<IPAllowlistTable
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<IPAllowlistModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTrustedIp.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteTrustedIp?.data as { name: string })?.name || " "
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteTrustedIpSubmit((popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">IP Allowlist</h2>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("trustedIp");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
isLoading={false}
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add IP
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<IPAllowlistTable
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<IPAllowlistModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTrustedIp.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteTrustedIp?.data as { name: string })?.name || " "
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteTrustedIpSubmit(
|
||||
(popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,92 +1,81 @@
|
||||
import { faGlobe, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetTrustedIps
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useGetTrustedIps } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["upgradePlan"]>;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["trustedIp", "deleteTrustedIp", "upgradePlan"]>,
|
||||
data?: {
|
||||
trustedIpId: string;
|
||||
ipAddress?: string;
|
||||
comment?: string;
|
||||
isActive?: boolean;
|
||||
prefix?: number;
|
||||
},
|
||||
) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
|
||||
popUp: UsePopUpState<["upgradePlan"]>;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["trustedIp", "deleteTrustedIp", "upgradePlan"]>,
|
||||
data?: {
|
||||
trustedIpId: string;
|
||||
ipAddress?: string;
|
||||
comment?: string;
|
||||
isActive?: boolean;
|
||||
prefix?: number;
|
||||
}
|
||||
) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const IPAllowlistTable = ({
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
|
||||
|
||||
const formatType = (type: string, prefix?: number) => {
|
||||
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${(prefix !== undefined) ? "CIDR" : ""}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">IP Address / Range</Th>
|
||||
<Th className="flex-1">Format</Th>
|
||||
<Th className="flex-1">Comment</Th>
|
||||
{/* <Th className="flex-1">Status</Th> */}
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && data && data?.length > 0 && data
|
||||
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
|
||||
.map(({
|
||||
_id,
|
||||
ipAddress,
|
||||
comment,
|
||||
type,
|
||||
prefix,
|
||||
isActive
|
||||
}) => {
|
||||
return (
|
||||
<Tr
|
||||
key={`ip-access-range-${_id}`}
|
||||
className="h-10"
|
||||
>
|
||||
<Td>
|
||||
{`${ipAddress}${(prefix !== undefined) ? `/${prefix}` : ""}`}
|
||||
</Td>
|
||||
<Td>
|
||||
{formatType(type, prefix)}
|
||||
</Td>
|
||||
<Td>
|
||||
{comment}
|
||||
</Td>
|
||||
{/* <Td>
|
||||
export const IPAllowlistTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
|
||||
|
||||
const formatType = (type: string, prefix?: number) => {
|
||||
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${
|
||||
prefix !== undefined ? "CIDR" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">IP Address / Range</Th>
|
||||
<Th className="flex-1">Format</Th>
|
||||
<Th className="flex-1">Comment</Th>
|
||||
{/* <Th className="flex-1">Status</Th> */}
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data
|
||||
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
|
||||
.map(({ _id, ipAddress, comment, type, prefix, isActive }) => {
|
||||
return (
|
||||
<Tr key={`ip-access-range-${_id}`} className="h-10">
|
||||
<Td>{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}</Td>
|
||||
<Td>{formatType(type, prefix)}</Td>
|
||||
<Td>{comment}</Td>
|
||||
{/* <Td>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
@@ -95,68 +84,83 @@ export const IPAllowlistTable = ({
|
||||
<p className="ml-4">Active</p>
|
||||
</div>
|
||||
</Td> */}
|
||||
<Td className="flex items-center">
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("trustedIp", {
|
||||
trustedIpId: _id,
|
||||
ipAddress,
|
||||
comment,
|
||||
prefix,
|
||||
isActive
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("deleteTrustedIp", {
|
||||
trustedIpId: _id
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No IP addresses added"
|
||||
icon={faGlobe}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Td className="flex items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("trustedIp", {
|
||||
trustedIpId: _id,
|
||||
ipAddress,
|
||||
comment,
|
||||
prefix,
|
||||
isActive
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.IpAllowList}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
handlePopUpOpen("deleteTrustedIp", {
|
||||
trustedIpId: _id
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && (
|
||||
<TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />
|
||||
)}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No IP addresses added" icon={faGlobe} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
@@ -15,44 +16,50 @@ enum TabSections {
|
||||
Roles = "roles"
|
||||
}
|
||||
|
||||
export const MembersPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const orgId = currentWorkspace?.organization || "";
|
||||
export const MembersPage = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const orgId = currentWorkspace?.organization || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">{t("settings.members.title")}</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
||||
{t("settings.members.title")}
|
||||
</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<MemberListTab roles={roles as TRole<string>[]} />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<ProjectRoleListTab roles={roles as TRole<string>[]} isRolesLoading={isRolesLoading} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<MemberListTab roles={roles as TRole<string>[]} />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<ProjectRoleListTab
|
||||
roles={roles as TRole<string>[]}
|
||||
isRolesLoading={isRolesLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Member }
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
import {
|
||||
GeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useUser,
|
||||
useWorkspace
|
||||
@@ -240,7 +242,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
@@ -250,7 +252,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -276,9 +278,9 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={GeneralPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<>
|
||||
@@ -316,13 +318,13 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?._id && (
|
||||
<OrgPermissionCan
|
||||
I={GeneralPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -336,7 +338,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
@@ -11,35 +13,38 @@ type Props = {
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
export const ProjectRoleListTab = withProjectPermission(
|
||||
({ roles = [], isRolesLoading }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
|
||||
return popUp.editRole.isOpen ? (
|
||||
<motion.div
|
||||
key="role-modify"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleModifySection
|
||||
role={popUp.editRole.data as TRole<string>}
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<ProjectRoleList
|
||||
roles={roles}
|
||||
isRolesLoading={isRolesLoading}
|
||||
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
return popUp.editRole.isOpen ? (
|
||||
<motion.div
|
||||
key="role-modify"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleModifySection
|
||||
role={popUp.editRole.data as TRole<string>}
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<ProjectRoleList
|
||||
roles={roles}
|
||||
isRolesLoading={isRolesLoading}
|
||||
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -19,7 +20,12 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRole } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
@@ -69,9 +75,17 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
placeholder="Search roles..."
|
||||
/>
|
||||
</div>
|
||||
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
|
||||
Add Role
|
||||
</Button>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => onSelectRole()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -99,27 +113,48 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Tooltip content="Edit">
|
||||
<IconButton
|
||||
ariaLabel="edit"
|
||||
onClick={() => onSelectRole(role)}
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||
variant="plain"
|
||||
isDisabled={isNonMutatable}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="Edit">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit"
|
||||
onClick={() => onSelectRole(role)}
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip
|
||||
content={
|
||||
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||
variant="plain"
|
||||
isDisabled={isNonMutatable || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
import { SingleProjectPermission } from "./SingleProjectPermission";
|
||||
import { WsProjectPermission } from "./WsProjectPermission";
|
||||
|
||||
const SINGLE_PERMISSION_LIST = [
|
||||
{
|
||||
@@ -271,6 +272,13 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col space-y-4" key="permission-ws">
|
||||
<WsProjectPermission
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isNonEditable={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 mt-12">
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "edit", label: "Update" },
|
||||
{ action: "delete", label: "Remove" }
|
||||
] as const;
|
||||
|
||||
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.workspace"
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
|
||||
break;
|
||||
default:
|
||||
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"px-10 py-6 bg-mineshaft-800 rounded-md",
|
||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="font-medium mb-1 text-lg">Workspace</div>
|
||||
<div className="text-xs font-light">Workspace control actions</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
||||
>
|
||||
{isCustom &&
|
||||
PERMISSIONS.map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.workspace.${action}`}
|
||||
key={`permissions.workspace.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.workspace.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,7 +23,13 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useCreateSecretV3,
|
||||
useDeleteSecretV3,
|
||||
@@ -38,7 +44,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||
|
||||
export const SecretOverviewPage = () => {
|
||||
const SecretOverview = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const router = useRouter();
|
||||
@@ -394,3 +400,8 @@ export const SecretOverviewPage = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
|
||||
action: ProjectPermissionActions.Read,
|
||||
subject: ProjectPermissionSub.Secrets
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@ import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-i
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
@@ -91,19 +93,26 @@ export const SecretEditRow = ({
|
||||
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
|
||||
{isDirty ? (
|
||||
<>
|
||||
<div>
|
||||
<Tooltip content="save">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="submit-value"
|
||||
className="h-full"
|
||||
isDisabled={isSubmitting}
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="save">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="submit-value"
|
||||
className="h-full"
|
||||
isDisabled={isSubmitting || !isAllowed}
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div>
|
||||
<Tooltip content="cancel">
|
||||
<IconButton
|
||||
@@ -132,19 +141,26 @@ export const SecretEditRow = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="delete-value"
|
||||
className="h-full"
|
||||
onClick={handleDeleteSecret}
|
||||
isDisabled={isDeleting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="delete-value"
|
||||
className="h-full"
|
||||
onClick={handleDeleteSecret}
|
||||
isDisabled={isDeleting || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
||||
@@ -14,45 +16,50 @@ const tabs = [
|
||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||
];
|
||||
|
||||
export const ProjectSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||
<div className="w-full max-w-screen-lg">
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
||||
export const ProjectSettingsPage = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||
<div className="w-full max-w-screen-lg">
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
|
||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<ProjectGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ProjectServiceTokensTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<WebhooksTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<ProjectGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ProjectServiceTokensTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<WebhooksTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
@@ -1,52 +1,62 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||
|
||||
export const AutoCapitalizationSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync } = useToggleAutoCapitalization();
|
||||
|
||||
const handleToggleCapitalizationToggle = async (state: boolean) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
export const AutoCapitalizationSection = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync } = useToggleAutoCapitalization();
|
||||
|
||||
await mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
state
|
||||
});
|
||||
const handleToggleCapitalizationToggle = async (state: boolean) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
|
||||
createNotification({
|
||||
text,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update auto capitalization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
await mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
state
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleCapitalizationToggle(state as boolean);
|
||||
}}
|
||||
>
|
||||
{t("settings.project.auto-capitalization-description")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
|
||||
createNotification({
|
||||
text,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update auto capitalization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleCapitalizationToggle(state as boolean);
|
||||
}}
|
||||
>
|
||||
{t("settings.project.auto-capitalization-description")}
|
||||
</Checkbox>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
@@ -3,30 +3,34 @@ import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useDeleteWorkspace
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useDeleteWorkspace } from "@app/hooks/api";
|
||||
|
||||
export const DeleteProjectSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization()
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState("");
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState("");
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
|
||||
const onDeleteWorkspace = async () => {
|
||||
const onDeleteWorkspace = async () => {
|
||||
setIsDeleting.on();
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
await deleteWorkspace.mutateAsync({
|
||||
if (!currentWorkspace?._id) return;
|
||||
await deleteWorkspace.mutateAsync({
|
||||
workspaceID: currentWorkspace?._id
|
||||
});
|
||||
});
|
||||
// redirect user to the org overview
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
|
||||
@@ -45,38 +49,42 @@ export const DeleteProjectSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
|
||||
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
|
||||
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
|
||||
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
|
||||
<FormControl
|
||||
label={
|
||||
<div className="mb-0.5 text-sm font-normal text-gray-400">
|
||||
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
|
||||
workspace
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => setDeleteProjectInput(e.target.value)}
|
||||
value={deleteProjectInput}
|
||||
placeholder="Type the project name to delete"
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
|
||||
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
|
||||
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
|
||||
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
|
||||
<FormControl
|
||||
label={
|
||||
<div className="mb-0.5 text-sm font-normal text-gray-400">
|
||||
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
|
||||
workspace
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => setDeleteProjectInput(e.target.value)}
|
||||
value={deleteProjectInput}
|
||||
placeholder="Type the project name to delete"
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={onDeleteWorkspace}
|
||||
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
{t("settings.project.delete-project")}
|
||||
</Button>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t("settings.project.delete-project-note")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t("settings.project.delete-project-note")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,99 +1,115 @@
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetUserWsKey,useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
|
||||
export const E2EESection = () => {
|
||||
export const E2EESection = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
|
||||
/**
|
||||
* Activate bot for project by performing the following steps:
|
||||
* 1. Get the (encrypted) project key
|
||||
* 2. Decrypt project key with user's private key
|
||||
* 3. Encrypt project key with bot's public key
|
||||
* 4. Send encrypted project key to backend and set bot status to active
|
||||
*/
|
||||
* Activate bot for project by performing the following steps:
|
||||
* 1. Get the (encrypted) project key
|
||||
* 2. Decrypt project key with user's private key
|
||||
* 3. Encrypt project key with bot's public key
|
||||
* 4. Send encrypted project key to backend and set bot status to active
|
||||
*/
|
||||
const toggleBotActivate = async () => {
|
||||
let botKey;
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
let botKey;
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
if (bot && wsKey) {
|
||||
// case: there is a bot
|
||||
|
||||
if (!bot.isActive) {
|
||||
// bot is not active -> activate bot
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
if (bot && wsKey) {
|
||||
// case: there is a bot
|
||||
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error("Private Key missing");
|
||||
}
|
||||
if (!bot.isActive) {
|
||||
// bot is not active -> activate bot
|
||||
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: bot.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
botKey = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
await updateBotActiveStatus({
|
||||
workspaceId: currentWorkspace._id,
|
||||
botKey,
|
||||
isActive: true,
|
||||
botId: bot._id
|
||||
});
|
||||
} else {
|
||||
// bot is active -> deactivate bot
|
||||
await updateBotActiveStatus({
|
||||
isActive: false,
|
||||
botId: bot._id,
|
||||
workspaceId: currentWorkspace._id
|
||||
});
|
||||
}
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error("Private Key missing");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: bot.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
botKey = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
await updateBotActiveStatus({
|
||||
workspaceId: currentWorkspace._id,
|
||||
botKey,
|
||||
isActive: true,
|
||||
botId: bot._id
|
||||
});
|
||||
} else {
|
||||
// bot is active -> deactivate bot
|
||||
await updateBotActiveStatus({
|
||||
isActive: false,
|
||||
botId: bot._id,
|
||||
workspaceId: currentWorkspace._id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return bot ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
||||
read/decrypt your secret values.
|
||||
</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
isDisabled={!isAllowed}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
) : <div />;
|
||||
};
|
||||
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
},
|
||||
{
|
||||
action: ProjectPermissionActions.Read,
|
||||
subject: ProjectPermissionSub.Settings
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,115 +2,127 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDeleteWsEnvironment } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddEnvironmentModal } from "./AddEnvironmentModal";
|
||||
import { EnvironmentTable } from "./EnvironmentTable";
|
||||
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
|
||||
|
||||
export const EnvironmentSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
export const EnvironmentSection = withProjectPermission(
|
||||
() => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const deleteWsEnvironment = useDeleteWsEnvironment();
|
||||
const deleteWsEnvironment = useDeleteWsEnvironment();
|
||||
|
||||
const isMoreEnvironmentsAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"createEnv",
|
||||
"updateEnv",
|
||||
"deleteEnv",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const isMoreEnvironmentsAllowed =
|
||||
subscription?.environmentLimit && currentWorkspace?.environments
|
||||
? currentWorkspace.environments.length < subscription.environmentLimit
|
||||
: true;
|
||||
|
||||
const onEnvDeleteSubmit = async (environmentSlug: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
await deleteWsEnvironment.mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
environmentSlug
|
||||
});
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"createEnv",
|
||||
"updateEnv",
|
||||
"deleteEnv",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted environment",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteEnv");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete environment",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
const onEnvDeleteSubmit = async (environmentSlug: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Environments
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreEnvironmentsAllowed) {
|
||||
handlePopUpOpen("createEnv");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create environment
|
||||
</Button>
|
||||
await deleteWsEnvironment.mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
environmentSlug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted environment",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteEnv");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete environment",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreEnvironmentsAllowed) {
|
||||
handlePopUpOpen("createEnv");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create environment
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpdateEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteEnv.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || " "
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteEnv", isOpen)}
|
||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
|
||||
onDeleteApproved={() =>
|
||||
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Choose which environments will show up in your dashboard like development, staging, production
|
||||
</p>
|
||||
<EnvironmentTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpdateEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteEnv.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || " "
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteEnv", isOpen)}
|
||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
|
||||
onDeleteApproved={() =>
|
||||
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-soli
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useReorderWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
@@ -115,28 +116,42 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug });
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteEnv", { name, slug });
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteEnv", { name, slug });
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
@@ -1,71 +1,77 @@
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets
|
||||
} from "@app/hooks/api";
|
||||
|
||||
export const ProjectIndexSecretsSection = () => {
|
||||
// TODO: add check so that this only shows up if user is
|
||||
// an admin in the workspace
|
||||
|
||||
export const ProjectIndexSecretsSection = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(currentWorkspace?._id ?? "");
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
||||
currentWorkspace?._id ?? ""
|
||||
);
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||
|
||||
const onEnableBlindIndices = async () => {
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!encryptedSecrets) return;
|
||||
if (!latestFileKey) return;
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!encryptedSecrets) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
|
||||
const secretName = decryptSymmetric({
|
||||
ciphertext: encryptedSecret.secretKeyCiphertext,
|
||||
iv: encryptedSecret.secretKeyIV,
|
||||
tag: encryptedSecret.secretKeyTag,
|
||||
key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
return {
|
||||
secretName,
|
||||
_id: encryptedSecret._id
|
||||
};
|
||||
});
|
||||
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
|
||||
const secretName = decryptSymmetric({
|
||||
ciphertext: encryptedSecret.secretKeyCiphertext,
|
||||
iv: encryptedSecret.secretKeyIV,
|
||||
tag: encryptedSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
await nameWorkspaceSecrets.mutateAsync({
|
||||
workspaceId: currentWorkspace._id,
|
||||
secretsToUpdate
|
||||
});
|
||||
};
|
||||
return {
|
||||
secretName,
|
||||
_id: encryptedSecret._id
|
||||
};
|
||||
});
|
||||
|
||||
return (!isBlindIndexedLoading && (isBlindIndexed === false)) ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
|
||||
</p>
|
||||
<Button
|
||||
onClick={onEnableBlindIndices}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
>
|
||||
Enable Blind Indexing
|
||||
</Button>
|
||||
</div>
|
||||
await nameWorkspaceSecrets.mutateAsync({
|
||||
workspaceId: currentWorkspace._id,
|
||||
secretsToUpdate
|
||||
});
|
||||
};
|
||||
|
||||
return !isBlindIndexedLoading && !isBlindIndexed ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Your project, created before the introduction of blind indexing, contains unindexed
|
||||
secrets. To access individual secrets by name through the SDK and public API, please
|
||||
enable blind indexing.
|
||||
</p>
|
||||
<Button onClick={onEnableBlindIndices} color="mineshaft" size="sm" type="submit">
|
||||
Enable Blind Indexing
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
<div />
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
||||
);
|
||||
|
||||
@@ -4,11 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useRenameWorkspace
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useRenameWorkspace } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name")
|
||||
@@ -21,25 +20,20 @@ export const ProjectNameChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
reset({
|
||||
name: currentWorkspace.name
|
||||
});
|
||||
}
|
||||
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
|
||||
await mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
newWorkspaceName: name
|
||||
@@ -49,7 +43,6 @@ export const ProjectNameChangeSection = () => {
|
||||
text: "Successfully renamed workspace",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
@@ -60,37 +53,35 @@ export const ProjectNameChangeSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
Project Name
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Project name" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteWsTag } from "@app/hooks/api";
|
||||
|
||||
@@ -14,74 +14,80 @@ import { SecretTagsTable } from "./SecretTagsTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const SecretTagsSection = (): JSX.Element => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"CreateSecretTag",
|
||||
"deleteTagConfirmation"
|
||||
] as const);
|
||||
export const SecretTagsSection = withProjectPermission(
|
||||
(): JSX.Element => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"CreateSecretTag",
|
||||
"deleteTagConfirmation"
|
||||
] as const);
|
||||
|
||||
const deleteWsTag = useDeleteWsTag();
|
||||
const deleteWsTag = useDeleteWsTag();
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
await deleteWsTag.mutateAsync({
|
||||
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
await deleteWsTag.mutateAsync({
|
||||
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted tag",
|
||||
type: "success"
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted tag",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTagConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete the tag",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
handlePopUpClose("deleteTagConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete the tag",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
console.log("x");
|
||||
handlePopUpOpen("CreateSecretTag");
|
||||
console.log("x2");
|
||||
}}
|
||||
>
|
||||
Create tag
|
||||
</Button>
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
console.log("x");
|
||||
handlePopUpOpen("CreateSecretTag");
|
||||
console.log("x2");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create tag
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
|
||||
current project.
|
||||
</p>
|
||||
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddSecretTagModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTagConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} api key?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTagConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteTagConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for
|
||||
the current project.
|
||||
</p>
|
||||
<SecretTagsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddSecretTagModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTagConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} api key?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTagConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteTagConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetWsTags } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -53,18 +54,26 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
name,
|
||||
id: _id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
name,
|
||||
id: _id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteServiceToken } from "@app/hooks/api";
|
||||
|
||||
@@ -12,65 +15,76 @@ import { ServiceTokenTable } from "./ServiceTokenTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ServiceTokenSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
export const ServiceTokenSection = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createAPIToken",
|
||||
"deleteAPITokenConfirmation"
|
||||
] as const);
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createAPIToken",
|
||||
"deleteAPITokenConfirmation"
|
||||
] as const);
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteServiceToken.mutateAsync(
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
|
||||
);
|
||||
createNotification({
|
||||
text: "Successfully deleted service token",
|
||||
type: "success"
|
||||
});
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteServiceToken.mutateAsync(
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
|
||||
);
|
||||
createNotification({
|
||||
text: "Successfully deleted service token",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteAPITokenConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
handlePopUpClose("deleteAPITokenConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
{t("section.token.service-tokens")}
|
||||
</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
}}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
{t("section.token.service-tokens")}
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} service token?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} service token?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -70,18 +71,26 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row._id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row._id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateWebhook,
|
||||
@@ -29,252 +31,291 @@ import {
|
||||
|
||||
import { AddWebhookForm, TFormSchema } from "./AddWebhookForm";
|
||||
|
||||
export const WebhooksTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWebhook",
|
||||
"deleteWebhook"
|
||||
] as const);
|
||||
export const WebhooksTab = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWebhook",
|
||||
"deleteWebhook"
|
||||
] as const);
|
||||
|
||||
const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId);
|
||||
const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId);
|
||||
|
||||
// mutation
|
||||
const { mutateAsync: createWebhook } = useCreateWebhook();
|
||||
const {
|
||||
mutateAsync: testWebhook,
|
||||
variables: testWebhookVars,
|
||||
isLoading: isTestWebhookSubmitting
|
||||
} = useTestWebhook();
|
||||
const {
|
||||
mutateAsync: updateWebhook,
|
||||
variables: updateWebhookVars,
|
||||
isLoading: isUpdateWebhookSubmitting
|
||||
} = useUpdateWebhook();
|
||||
const { mutateAsync: deleteWebhook } = useDeleteWebhook();
|
||||
// mutation
|
||||
const { mutateAsync: createWebhook } = useCreateWebhook();
|
||||
const {
|
||||
mutateAsync: testWebhook,
|
||||
variables: testWebhookVars,
|
||||
isLoading: isTestWebhookSubmitting
|
||||
} = useTestWebhook();
|
||||
const {
|
||||
mutateAsync: updateWebhook,
|
||||
variables: updateWebhookVars,
|
||||
isLoading: isUpdateWebhookSubmitting
|
||||
} = useUpdateWebhook();
|
||||
const { mutateAsync: deleteWebhook } = useDeleteWebhook();
|
||||
|
||||
const handleWebhookCreate = async (data: TFormSchema) => {
|
||||
try {
|
||||
await createWebhook({
|
||||
...data,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("addWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookCreate = async (data: TFormSchema) => {
|
||||
try {
|
||||
await createWebhook({
|
||||
...data,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("addWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
await updateWebhook({
|
||||
webhookId,
|
||||
workspaceId,
|
||||
isDisabled
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
await updateWebhook({
|
||||
webhookId,
|
||||
workspaceId,
|
||||
isDisabled
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookDelete = async () => {
|
||||
try {
|
||||
const webhookId = popUp?.deleteWebhook?.data as string;
|
||||
await deleteWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("deleteWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookDelete = async () => {
|
||||
try {
|
||||
const webhookId = popUp?.deleteWebhook?.data as string;
|
||||
await deleteWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("deleteWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookTest = async (webhookId: string) => {
|
||||
try {
|
||||
await testWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully triggered webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to trigger webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookTest = async (webhookId: string) => {
|
||||
try {
|
||||
await testWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully triggered webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to trigger webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addWebhook")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td>URL</Td>
|
||||
<Td>Environment</Td>
|
||||
<Td>Secret Path</Td>
|
||||
<Td>Status</Td>
|
||||
<Td className="text-right">Action</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||
return (
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addWebhook")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No webhooks found" icon={faPlug} />
|
||||
</Td>
|
||||
<Td>URL</Td>
|
||||
<Td>Environment</Td>
|
||||
<Td>Secret Path</Td>
|
||||
<Td>Status</Td>
|
||||
<Td className="text-right">Action</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isWebhooksLoading &&
|
||||
webhooks?.map(
|
||||
({
|
||||
_id: id,
|
||||
url,
|
||||
environment,
|
||||
secretPath,
|
||||
lastStatus,
|
||||
isDisabled,
|
||||
updatedAt,
|
||||
lastRunErrorMessage
|
||||
}) => (
|
||||
<Tr key={id}>
|
||||
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
{url}
|
||||
</Td>
|
||||
<Td>{environment}</Td>
|
||||
<Td>{secretPath}</Td>
|
||||
<Td>
|
||||
{!lastStatus ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="inline-flex w-min items-center rounded bg-mineshaft-600 px-2 py-0.5 text-sm">
|
||||
{lastStatus}{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
Updated At:{" "}
|
||||
{format(new Date(updatedAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{lastRunErrorMessage && (
|
||||
<div className="mt-2 text-red">
|
||||
Error: {lastRunErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={`ml-1 ${
|
||||
lastStatus === "failed" ? "text-red" : "text-green"
|
||||
}`}
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
variant="star"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookTest(id)}
|
||||
isDisabled={
|
||||
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
|
||||
}
|
||||
isLoading={isTestWebhookSubmitting && testWebhookVars?.webhookId === id}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookDisable(id, !isDisabled)}
|
||||
isDisabled={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
isLoading={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
onClick={() => handlePopUpOpen("deleteWebhook", id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
</THead>
|
||||
<TBody>
|
||||
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No webhooks found" icon={faPlug} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{!isWebhooksLoading &&
|
||||
webhooks?.map(
|
||||
({
|
||||
_id: id,
|
||||
url,
|
||||
environment,
|
||||
secretPath,
|
||||
lastStatus,
|
||||
isDisabled,
|
||||
updatedAt,
|
||||
lastRunErrorMessage
|
||||
}) => (
|
||||
<Tr key={id}>
|
||||
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
{url}
|
||||
</Td>
|
||||
<Td>{environment}</Td>
|
||||
<Td>{secretPath}</Td>
|
||||
<Td>
|
||||
{!lastStatus ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="inline-flex w-min items-center rounded bg-mineshaft-600 px-2 py-0.5 text-sm">
|
||||
{lastStatus}{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
Updated At:{" "}
|
||||
{format(new Date(updatedAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{lastRunErrorMessage && (
|
||||
<div className="mt-2 text-red">
|
||||
Error: {lastRunErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={`ml-1 ${
|
||||
lastStatus === "failed" ? "text-red" : "text-green"
|
||||
}`}
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="star"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookTest(id)}
|
||||
isDisabled={
|
||||
(isTestWebhookSubmitting &&
|
||||
testWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookDisable(id, !isDisabled)}
|
||||
isDisabled={
|
||||
(isUpdateWebhookSubmitting &&
|
||||
updateWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("deleteWebhook", id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<AddWebhookForm
|
||||
environments={currentWorkspace?.environments}
|
||||
isOpen={popUp?.addWebhook?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addWebhook", isOpen)}
|
||||
onCreateWebhook={handleWebhookCreate}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWebhook.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Are you sure you want to delete this webhook?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteWebhook", isOpen)}
|
||||
onClose={() => handlePopUpClose("deleteWebhook")}
|
||||
onDeleteApproved={handleWebhookDelete}
|
||||
/>
|
||||
</div>
|
||||
<AddWebhookForm
|
||||
environments={currentWorkspace?.environments}
|
||||
isOpen={popUp?.addWebhook?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addWebhook", isOpen)}
|
||||
onCreateWebhook={handleWebhookCreate}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWebhook.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Are you sure you want to delete this webhook?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteWebhook", isOpen)}
|
||||
onClose={() => handlePopUpClose("deleteWebhook")}
|
||||
onDeleteApproved={handleWebhookDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Webhooks }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user