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