mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-03 20:23:35 +00:00
Compare commits
3 Commits
feat/allow
...
feat/displ
Author | SHA1 | Date | |
---|---|---|---|
|
4a8dae5bb8 | ||
|
117617be78 | ||
|
ae928e225b |
@@ -1,45 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||
if (!hasSecretVersionV2UserActorId) {
|
||||
t.uuid("userActorId");
|
||||
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||
}
|
||||
if (!hasSecretVersionV2IdentityActorId) {
|
||||
t.uuid("identityActorId");
|
||||
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||
}
|
||||
if (!hasSecretVersionV2ActorType) {
|
||||
t.string("actorType");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||
if (hasSecretVersionV2UserActorId) {
|
||||
t.dropColumn("userActorId");
|
||||
}
|
||||
if (hasSecretVersionV2IdentityActorId) {
|
||||
t.dropColumn("identityActorId");
|
||||
}
|
||||
if (hasSecretVersionV2ActorType) {
|
||||
t.dropColumn("actorType");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Organization, "displayAllMembersInvite"))) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("displayAllMembersInvite").defaultTo(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Organization, "displayAllMembersInvite")) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("displayAllMembersInvite");
|
||||
});
|
||||
}
|
||||
}
|
@@ -22,7 +22,8 @@ export const OrganizationsSchema = z.object({
|
||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional()
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
displayAllMembersInvite: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -25,10 +25,7 @@ export const SecretVersionsV2Schema = z.object({
|
||||
folderId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userActorId: z.string().uuid().nullable().optional(),
|
||||
identityActorId: z.string().uuid().nullable().optional(),
|
||||
actorType: z.string().nullable().optional()
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||
|
@@ -503,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!hasMinApproval && !isSoftEnforcement)
|
||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
let mergeStatus;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
// this cycle if for bridged secrets
|
||||
@@ -861,6 +861,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
if (isSoftEnforcement) {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||
const approverUsers = await userDAL.find({
|
||||
@@ -1155,8 +1156,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
requestId: secretApprovalRequest.id
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1456,8 +1456,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
requestId: secretApprovalRequest.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -13,7 +13,6 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -333,7 +332,6 @@ export const secretRotationQueueFactory = ({
|
||||
await secretVersionV2BridgeDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
actorType: ActorType.PLATFORM,
|
||||
secretId: id
|
||||
})),
|
||||
tx
|
||||
|
@@ -7,7 +7,6 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -371,21 +370,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
const secrets = await secretV2BridgeDAL.insertMany(
|
||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||
secretVersions.map(
|
||||
({
|
||||
latestSecretVersion,
|
||||
version,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
secretId,
|
||||
envId,
|
||||
id,
|
||||
tags,
|
||||
// exclude the bottom fields from the secret - they are for versioning only.
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
...el
|
||||
}) => ({
|
||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
||||
...el,
|
||||
id: secretId,
|
||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||
@@ -416,18 +401,8 @@ export const secretSnapshotServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||
const actorType = actor || ActorType.PLATFORM;
|
||||
|
||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType
|
||||
})),
|
||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
||||
tx
|
||||
);
|
||||
await secretVersionV2TagBridgeDAL.insertMany(
|
||||
|
@@ -459,8 +459,7 @@ export const PROJECTS = {
|
||||
workspaceId: "The ID of the project to update.",
|
||||
name: "The new name of the project.",
|
||||
projectDescription: "An optional description label for the project.",
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||
slug: "An optional slug for the project. (must be unique within the organization)"
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||
},
|
||||
GET_KEY: {
|
||||
workspaceId: "The ID of the project to get the key from."
|
||||
|
@@ -83,14 +83,6 @@ const run = async () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (error) => {
|
||||
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
|
||||
});
|
||||
|
||||
await server.listen({
|
||||
port: envConfig.PORT,
|
||||
host: envConfig.HOST,
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||
|
||||
export enum QueueName {
|
||||
SecretRotation = "secret-rotation",
|
||||
@@ -108,7 +107,7 @@ export type TQueueJobTypes = {
|
||||
};
|
||||
[QueueName.SecretWebhook]: {
|
||||
name: QueueJobs.SecWebhook;
|
||||
payload: TWebhookPayloads;
|
||||
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||
};
|
||||
|
||||
[QueueName.AccessTokenStatusUpdate]:
|
||||
|
@@ -111,16 +111,7 @@ export const secretRawSchema = z.object({
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
actor: z
|
||||
.object({
|
||||
actorId: z.string().nullable().optional(),
|
||||
actorType: z.string().nullable().optional(),
|
||||
name: z.string().nullable().optional(),
|
||||
membershipId: z.string().nullable().optional()
|
||||
})
|
||||
.optional()
|
||||
.nullable()
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export const ProjectPermissionSchema = z.object({
|
||||
|
@@ -257,7 +257,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
scimEnabled: z.boolean().optional(),
|
||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||
enforceMfa: z.boolean().optional(),
|
||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
|
||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
||||
displayAllMembersInvite: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -307,17 +307,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
.max(256, { message: "Description must be 256 or fewer characters" })
|
||||
.optional()
|
||||
.describe(PROJECTS.UPDATE.projectDescription),
|
||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(
|
||||
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
|
||||
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
|
||||
)
|
||||
.max(64, { message: "Slug must be 64 characters or fewer" })
|
||||
.optional()
|
||||
.describe(PROJECTS.UPDATE.slug)
|
||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -335,8 +325,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
update: {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
autoCapitalization: req.body.autoCapitalization,
|
||||
slug: req.body.slug
|
||||
autoCapitalization: req.body.autoCapitalization
|
||||
},
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
|
@@ -380,48 +380,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/raw/id/:secretId",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secret: secretRawSchema.extend({
|
||||
secretPath: z.string(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { secretId } = req.params;
|
||||
const secret = await server.services.secret.getSecretByIdRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretId
|
||||
});
|
||||
|
||||
return { secret };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/raw/:secretName",
|
||||
|
@@ -772,10 +772,6 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
@@ -114,27 +114,20 @@ export const integrationAuthServiceFactory = ({
|
||||
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
|
||||
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
|
||||
|
||||
const filteredAuthorizations = await Promise.all(
|
||||
authorizations.map(async (auth) => {
|
||||
try {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: auth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
return Promise.all(
|
||||
authorizations.filter(async (auth) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: auth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
|
||||
} catch (error) {
|
||||
// user does not belong to the project that the integration auth belongs to
|
||||
return null;
|
||||
}
|
||||
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
})
|
||||
);
|
||||
|
||||
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
|
||||
};
|
||||
|
||||
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
|
||||
|
@@ -286,7 +286,16 @@ export const orgServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId,
|
||||
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod }
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
authEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
displayAllMembersInvite
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@@ -358,7 +367,8 @@ export const orgServiceFactory = ({
|
||||
scimEnabled,
|
||||
defaultMembershipRole,
|
||||
enforceMfa,
|
||||
selectedMfaMethod
|
||||
selectedMfaMethod,
|
||||
displayAllMembersInvite
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
|
@@ -72,6 +72,7 @@ export type TUpdateOrgDTO = {
|
||||
defaultMembershipRoleSlug: string;
|
||||
enforceMfa: boolean;
|
||||
selectedMfaMethod: MfaMethod;
|
||||
displayAllMembersInvite: boolean;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
@@ -563,24 +563,11 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (update.slug) {
|
||||
const existingProject = await projectDAL.findOne({
|
||||
slug: update.slug,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
if (existingProject && existingProject.id !== project.id) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedProject = await projectDAL.updateById(project.id, {
|
||||
name: update.name,
|
||||
description: update.description,
|
||||
autoCapitalization: update.autoCapitalization,
|
||||
enforceCapitalization: update.autoCapitalization,
|
||||
slug: update.slug
|
||||
enforceCapitalization: update.autoCapitalization
|
||||
});
|
||||
|
||||
return updatedProject;
|
||||
|
@@ -82,7 +82,6 @@ export type TUpdateProjectDTO = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
autoCapitalization?: boolean;
|
||||
slug?: string;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
@@ -613,9 +613,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
@@ -625,13 +622,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
|
||||
);
|
||||
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
|
@@ -5,7 +5,6 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
@@ -63,7 +62,6 @@ export const fnSecretBulkInsert = async ({
|
||||
resourceMetadataDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor,
|
||||
tx
|
||||
}: TFnSecretBulkInsert) => {
|
||||
const sanitizedInputSecrets = inputSecrets.map(
|
||||
@@ -92,10 +90,6 @@ export const fnSecretBulkInsert = async ({
|
||||
})
|
||||
);
|
||||
|
||||
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
|
||||
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
|
||||
const actorType = actor?.type || ActorType.PLATFORM;
|
||||
|
||||
const newSecrets = await secretDAL.insertMany(
|
||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||
tx
|
||||
@@ -112,9 +106,6 @@ export const fnSecretBulkInsert = async ({
|
||||
sanitizedInputSecrets.map((el) => ({
|
||||
...el,
|
||||
folderId,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||
})),
|
||||
tx
|
||||
@@ -166,13 +157,8 @@ export const fnSecretBulkUpdate = async ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
resourceMetadataDAL,
|
||||
actor
|
||||
resourceMetadataDAL
|
||||
}: TFnSecretBulkUpdate) => {
|
||||
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
|
||||
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
|
||||
const actorType = actor?.type || ActorType.PLATFORM;
|
||||
|
||||
const sanitizedInputSecrets = inputSecrets.map(
|
||||
({
|
||||
filter,
|
||||
@@ -230,10 +216,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
encryptedValue,
|
||||
reminderRepeatDays,
|
||||
folderId,
|
||||
secretId,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType
|
||||
secretId
|
||||
})
|
||||
),
|
||||
tx
|
||||
@@ -633,12 +616,6 @@ export const reshapeBridgeSecret = (
|
||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||
value: string;
|
||||
comment: string;
|
||||
userActorName?: string | null;
|
||||
identityActorName?: string | null;
|
||||
userActorId?: string | null;
|
||||
identityActorId?: string | null;
|
||||
membershipId?: string | null;
|
||||
actorType?: string | null;
|
||||
tags?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -659,14 +636,6 @@ export const reshapeBridgeSecret = (
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
user: secret.userId,
|
||||
actor: secret.actorType
|
||||
? {
|
||||
actorType: secret.actorType,
|
||||
actorId: secret.userActorId || secret.identityActorId,
|
||||
name: secret.identityActorName || secret.userActorName,
|
||||
membershipId: secret.membershipId
|
||||
}
|
||||
: undefined,
|
||||
tags: secret.tags,
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
||||
|
@@ -28,7 +28,6 @@ import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||
import { TGetASecretByIdDTO } from "../secret/secret-types";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
@@ -74,13 +73,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
| "findBySecretPath"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "findByManySecretPath"
|
||||
| "find"
|
||||
| "findBySecretPathMultiEnv"
|
||||
| "findSecretPathByFolderIds"
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||
@@ -308,10 +301,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -494,10 +483,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -962,73 +947,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
|
||||
const secret = await secretDAL.findOneWithTags({
|
||||
[`${TableName.SecretV2}.id` as "id"]: secretId
|
||||
});
|
||||
|
||||
if (!secret) {
|
||||
throw new NotFoundError({
|
||||
message: `Secret with ID '${secretId}' not found`,
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]);
|
||||
|
||||
if (!folderWithPath) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with id '${secret.folderId}' not found`,
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: secret.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: folderWithPath.environmentSlug,
|
||||
secretPath: folderWithPath.path,
|
||||
secretName: secret.key,
|
||||
secretTags: secret.tags.map((i) => i.slug)
|
||||
})
|
||||
);
|
||||
|
||||
if (secret.type === SecretType.Personal && secret.userId !== actorId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "You are not allowed to access this secret",
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: secret.projectId
|
||||
});
|
||||
|
||||
const secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: "";
|
||||
|
||||
const secretComment = secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: "";
|
||||
|
||||
return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, {
|
||||
...secret,
|
||||
value: secretValue,
|
||||
comment: secretComment
|
||||
});
|
||||
};
|
||||
|
||||
const getSecretByName = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -1312,10 +1230,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -1576,10 +1490,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
resourceMetadataDAL
|
||||
});
|
||||
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||
@@ -1612,10 +1522,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
});
|
||||
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||
@@ -1783,19 +1689,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
});
|
||||
|
||||
return secretVersions.map((el) => {
|
||||
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
||||
return secretVersions.map((el) =>
|
||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// this is a backfilling API for secret references
|
||||
@@ -2055,10 +1956,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||
return {
|
||||
type: doc.type,
|
||||
@@ -2085,10 +1982,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
tx,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||
return {
|
||||
filter: {
|
||||
@@ -2311,7 +2204,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsMultiEnv,
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById
|
||||
getSecretsByFolderMappings
|
||||
};
|
||||
};
|
||||
|
@@ -168,10 +168,6 @@ export type TFnSecretBulkInsert = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRequireReferenceIfValue =
|
||||
@@ -196,10 +192,6 @@ export type TFnSecretBulkUpdate = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
};
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
|
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
|
||||
@@ -120,67 +119,11 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||
};
|
||||
|
||||
const findVersionsBySecretIdWithActors = async (
|
||||
secretId: string,
|
||||
projectId: string,
|
||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.SecretVersionV2)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||
.leftJoin(
|
||||
TableName.ProjectMembership,
|
||||
`${TableName.ProjectMembership}.userId`,
|
||||
`${TableName.SecretVersionV2}.userActorId`
|
||||
)
|
||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||
.where((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretVersionV2),
|
||||
`${TableName.Users}.username as userActorName`,
|
||||
`${TableName.Identity}.name as identityActorName`,
|
||||
`${TableName.ProjectMembership}.id as membershipId`
|
||||
);
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(
|
||||
sort.map(([column, order, nulls]) => ({
|
||||
column: `${TableName.SecretVersionV2}.${column as string}`,
|
||||
order,
|
||||
nulls
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const docs: Array<
|
||||
TSecretVersionsV2 & {
|
||||
userActorName: string | undefined | null;
|
||||
identityActorName: string | undefined | null;
|
||||
membershipId: string | undefined | null;
|
||||
}
|
||||
> = await query;
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretVersionV2Orm,
|
||||
pruneExcessVersions,
|
||||
findLatestVersionMany,
|
||||
bulkUpdate,
|
||||
findLatestVersionByFolderId,
|
||||
findVersionsBySecretIdWithActors
|
||||
findLatestVersionByFolderId
|
||||
};
|
||||
};
|
||||
|
@@ -579,7 +579,6 @@ export const fnSecretBulkInsert = async ({
|
||||
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
||||
}))
|
||||
);
|
||||
|
||||
const secretVersions = await secretVersionDAL.insertMany(
|
||||
sanitizedInputSecrets.map((el) => ({
|
||||
...el,
|
||||
|
@@ -61,7 +61,6 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
import { WebhookEvents } from "../webhook/webhook-types";
|
||||
import { TSecretDALFactory } from "./secret-dal";
|
||||
import { interpolateSecrets } from "./secret-fns";
|
||||
import {
|
||||
@@ -624,14 +623,7 @@ export const secretQueueFactory = ({
|
||||
await queueService.queue(
|
||||
QueueName.SecretWebhook,
|
||||
QueueJobs.SecWebhook,
|
||||
{
|
||||
type: WebhookEvents.SecretModified,
|
||||
payload: {
|
||||
environment,
|
||||
projectId,
|
||||
secretPath
|
||||
}
|
||||
},
|
||||
{ environment, projectId, secretPath },
|
||||
{
|
||||
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
|
||||
removeOnFail: { count: 5 },
|
||||
@@ -1063,8 +1055,6 @@ export const secretQueueFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgByProjectId(projectId);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const secret = await secretV2BridgeDAL.findById(data.secretId);
|
||||
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
|
||||
|
||||
if (!organization) {
|
||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
|
||||
@@ -1093,19 +1083,6 @@ export const secretQueueFactory = ({
|
||||
organizationName: organization.name
|
||||
}
|
||||
});
|
||||
|
||||
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
|
||||
type: WebhookEvents.SecretReminderExpired,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
projectId: project.id,
|
||||
secretPath: folder?.path,
|
||||
environment: folder?.environmentSlug || "",
|
||||
reminderNote: data.note,
|
||||
secretName: secret?.key,
|
||||
secretId: data.secretId
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const startSecretV2Migration = async (projectId: string) => {
|
||||
@@ -1513,17 +1490,14 @@ export const secretQueueFactory = ({
|
||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: job.data.payload.projectId
|
||||
projectId: job.data.projectId
|
||||
});
|
||||
|
||||
await fnTriggerWebhook({
|
||||
projectId: job.data.payload.projectId,
|
||||
environment: job.data.payload.environment,
|
||||
secretPath: job.data.payload.secretPath || "/",
|
||||
...job.data,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
webhookDAL,
|
||||
event: job.data,
|
||||
projectDAL,
|
||||
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
||||
});
|
||||
});
|
||||
|
@@ -71,7 +71,6 @@ import {
|
||||
TDeleteManySecretRawDTO,
|
||||
TDeleteSecretDTO,
|
||||
TDeleteSecretRawDTO,
|
||||
TGetASecretByIdRawDTO,
|
||||
TGetASecretDTO,
|
||||
TGetASecretRawDTO,
|
||||
TGetSecretAccessListDTO,
|
||||
@@ -96,7 +95,7 @@ type TSecretServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
|
||||
>;
|
||||
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
@@ -1383,18 +1382,6 @@ export const secretServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => {
|
||||
const secret = await secretV2BridgeService.getSecretById({
|
||||
secretId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
});
|
||||
|
||||
return secret;
|
||||
};
|
||||
|
||||
const getSecretByNameRaw = async ({
|
||||
type,
|
||||
path,
|
||||
@@ -3101,7 +3088,6 @@ export const secretServiceFactory = ({
|
||||
getSecretsRawMultiEnv,
|
||||
getSecretReferenceTree,
|
||||
getSecretsRawByFolderMappings,
|
||||
getSecretAccessList,
|
||||
getSecretByIdRaw
|
||||
getSecretAccessList
|
||||
};
|
||||
};
|
||||
|
@@ -121,10 +121,6 @@ export type TGetASecretDTO = {
|
||||
version?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretByIdDTO = {
|
||||
secretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateBulkSecretDTO = {
|
||||
path: string;
|
||||
environment: string;
|
||||
@@ -217,10 +213,6 @@ export type TGetASecretRawDTO = {
|
||||
projectId?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetASecretByIdRawDTO = {
|
||||
secretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSecretRawDTO = TProjectPermission & {
|
||||
secretName: string;
|
||||
secretPath: string;
|
||||
|
@@ -50,7 +50,6 @@ const buildSlackPayload = (notification: TSlackNotification) => {
|
||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||
*Environment*: ${payload.environment}
|
||||
*Secret path*: ${payload.secretPath || "/"}
|
||||
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
|
||||
|
||||
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
|
||||
payload.requestId
|
||||
|
@@ -62,7 +62,6 @@ export type TSlackNotification =
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
secretKeys: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
|
||||
import { WebhookType } from "./webhook-types";
|
||||
|
||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||
|
||||
@@ -54,64 +54,29 @@ export const triggerWebhookRequest = async (
|
||||
return req;
|
||||
};
|
||||
|
||||
export const getWebhookPayload = (event: TWebhookPayloads) => {
|
||||
if (event.type === WebhookEvents.SecretModified) {
|
||||
const { projectName, projectId, environment, secretPath, type } = event.payload;
|
||||
|
||||
switch (type) {
|
||||
case WebhookType.SLACK:
|
||||
return {
|
||||
text: "A secret value has been added or modified.",
|
||||
attachments: [
|
||||
{
|
||||
color: "#E7F256",
|
||||
fields: [
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
value: environment,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Secret Path",
|
||||
value: secretPath,
|
||||
short: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
case WebhookType.GENERAL:
|
||||
default:
|
||||
return {
|
||||
event: event.type,
|
||||
project: {
|
||||
workspaceId: projectId,
|
||||
projectName,
|
||||
environment,
|
||||
secretPath
|
||||
}
|
||||
};
|
||||
}
|
||||
export const getWebhookPayload = (
|
||||
eventName: string,
|
||||
details: {
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
}
|
||||
|
||||
const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
|
||||
) => {
|
||||
const { workspaceName, workspaceId, environment, secretPath, type } = details;
|
||||
|
||||
switch (type) {
|
||||
case WebhookType.SLACK:
|
||||
return {
|
||||
text: "You have a secret reminder",
|
||||
text: "A secret value has been added or modified.",
|
||||
attachments: [
|
||||
{
|
||||
color: "#E7F256",
|
||||
fields: [
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
value: workspaceName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
@@ -123,16 +88,6 @@ export const getWebhookPayload = (event: TWebhookPayloads) => {
|
||||
title: "Secret Path",
|
||||
value: secretPath,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Secret Name",
|
||||
value: secretName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Reminder Note",
|
||||
value: reminderNote,
|
||||
short: false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -141,14 +96,11 @@ export const getWebhookPayload = (event: TWebhookPayloads) => {
|
||||
case WebhookType.GENERAL:
|
||||
default:
|
||||
return {
|
||||
event: event.type,
|
||||
event: eventName,
|
||||
project: {
|
||||
workspaceId: projectId,
|
||||
projectName,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
reminderNote
|
||||
secretPath
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -158,7 +110,6 @@ export type TFnTriggerWebhookDTO = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
event: TWebhookPayloads;
|
||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
@@ -173,9 +124,8 @@ export const fnTriggerWebhook = async ({
|
||||
projectId,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
event,
|
||||
secretManagerDecryptor,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
secretManagerDecryptor
|
||||
}: TFnTriggerWebhookDTO) => {
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||
const toBeTriggeredHooks = webhooks.filter(
|
||||
@@ -184,20 +134,21 @@ export const fnTriggerWebhook = async ({
|
||||
);
|
||||
if (!toBeTriggeredHooks.length) return;
|
||||
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
|
||||
let { projectName } = event.payload;
|
||||
if (!projectName) {
|
||||
const project = await projectDAL.findById(event.payload.projectId);
|
||||
projectName = project.name;
|
||||
}
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const webhooksTriggered = await Promise.allSettled(
|
||||
toBeTriggeredHooks.map((hook) => {
|
||||
const formattedEvent = {
|
||||
type: event.type,
|
||||
payload: { ...event.payload, type: hook.type, projectName }
|
||||
} as TWebhookPayloads;
|
||||
return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
|
||||
})
|
||||
toBeTriggeredHooks.map((hook) =>
|
||||
triggerWebhookRequest(
|
||||
hook,
|
||||
secretManagerDecryptor,
|
||||
getWebhookPayload("secrets.modified", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
type: hook.type
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// filter hooks by status
|
||||
|
@@ -16,8 +16,7 @@ import {
|
||||
TDeleteWebhookDTO,
|
||||
TListWebhookDTO,
|
||||
TTestWebhookDTO,
|
||||
TUpdateWebhookDTO,
|
||||
WebhookEvents
|
||||
TUpdateWebhookDTO
|
||||
} from "./webhook-types";
|
||||
|
||||
type TWebhookServiceFactoryDep = {
|
||||
@@ -145,15 +144,12 @@ export const webhookServiceFactory = ({
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
|
||||
getWebhookPayload({
|
||||
type: "test" as WebhookEvents.SecretModified,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
projectId: webhook.projectId,
|
||||
environment: webhook.environment.slug,
|
||||
secretPath: webhook.secretPath,
|
||||
type: webhook.type
|
||||
}
|
||||
getWebhookPayload("test", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: webhook.projectId,
|
||||
environment: webhook.environment.slug,
|
||||
secretPath: webhook.secretPath,
|
||||
type: webhook.type
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
|
@@ -30,36 +30,3 @@ export enum WebhookType {
|
||||
GENERAL = "general",
|
||||
SLACK = "slack"
|
||||
}
|
||||
|
||||
export enum WebhookEvents {
|
||||
SecretModified = "secrets.modified",
|
||||
SecretReminderExpired = "secrets.reminder-expired",
|
||||
TestEvent = "test"
|
||||
}
|
||||
|
||||
type TWebhookSecretModifiedEventPayload = {
|
||||
type: WebhookEvents.SecretModified;
|
||||
payload: {
|
||||
projectName?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type TWebhookSecretReminderEventPayload = {
|
||||
type: WebhookEvents.SecretReminderExpired;
|
||||
payload: {
|
||||
projectName?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
secretName: string;
|
||||
secretId: string;
|
||||
reminderNote?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;
|
||||
|
@@ -36,18 +36,3 @@ If the signature in the header matches the signature that you generated, then yo
|
||||
"timestamp": ""
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "secrets.reminder-expired",
|
||||
"project": {
|
||||
"workspaceId": "the workspace id",
|
||||
"environment": "project environment",
|
||||
"secretPath": "project folder path",
|
||||
"secretName": "name of the secret",
|
||||
"secretId": "id of the secret",
|
||||
"reminderNote": "reminder note of the secret"
|
||||
},
|
||||
"timestamp": ""
|
||||
}
|
||||
```
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -32,7 +33,13 @@ import {
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetExternalKmsList,
|
||||
useGetUserWorkspaces
|
||||
} from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -44,6 +51,7 @@ const formSchema = z.object({
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional(),
|
||||
addMembers: z.boolean().default(false),
|
||||
kmsKeyId: z.string(),
|
||||
template: z.string()
|
||||
});
|
||||
@@ -65,6 +73,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const { user } = useUser();
|
||||
const createWs = useCreateWorkspace();
|
||||
const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const canReadProjectTemplates = permission.can(
|
||||
@@ -102,6 +111,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const onCreateProject = async ({
|
||||
name,
|
||||
description,
|
||||
addMembers,
|
||||
kmsKeyId,
|
||||
template
|
||||
}: TAddProjectFormData) => {
|
||||
@@ -118,6 +128,21 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
template,
|
||||
type: projectType
|
||||
});
|
||||
const { id: newProjectId } = project;
|
||||
|
||||
if (addMembers) {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
}
|
||||
|
||||
await refetchWorkspaces();
|
||||
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
@@ -221,7 +246,33 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex">
|
||||
{currentOrg?.displayAllMembersInvite && (
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue={false}
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!isAllowed}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-14 flex">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
|
@@ -109,7 +109,8 @@ export const useUpdateOrg = () => {
|
||||
orgId,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
selectedMfaMethod
|
||||
selectedMfaMethod,
|
||||
displayAllMembersInvite
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@@ -118,7 +119,8 @@ export const useUpdateOrg = () => {
|
||||
slug,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
selectedMfaMethod
|
||||
selectedMfaMethod,
|
||||
displayAllMembersInvite
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@@ -15,6 +15,7 @@ export type Organization = {
|
||||
defaultMembershipRole: string;
|
||||
enforceMfa: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
displayAllMembersInvite?: boolean;
|
||||
};
|
||||
|
||||
export type UpdateOrgDTO = {
|
||||
@@ -26,6 +27,7 @@ export type UpdateOrgDTO = {
|
||||
defaultMembershipRoleSlug?: string;
|
||||
enforceMfa?: boolean;
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
displayAllMembersInvite?: boolean;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
@@ -101,12 +101,6 @@ export type SecretVersions = {
|
||||
skipMultilineEncoding?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
actor?: {
|
||||
actorId?: string | null;
|
||||
actorType?: string | null;
|
||||
name?: string | null;
|
||||
membershipId?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
// dto
|
||||
|
@@ -251,13 +251,12 @@ export const useUpdateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Workspace, object, UpdateProjectDTO>({
|
||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription, newSlug }) => {
|
||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
|
||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${projectID}`,
|
||||
{
|
||||
name: newProjectName,
|
||||
description: newProjectDescription,
|
||||
slug: newSlug
|
||||
description: newProjectDescription
|
||||
}
|
||||
);
|
||||
return data.workspace;
|
||||
|
@@ -74,7 +74,6 @@ export type UpdateProjectDTO = {
|
||||
projectID: string;
|
||||
newProjectName: string;
|
||||
newProjectDescription?: string;
|
||||
newSlug?: string;
|
||||
};
|
||||
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<ProjectOverviewChangeSection />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -9,7 +9,9 @@ import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
@@ -18,55 +20,31 @@ const baseFormSchema = z.object({
|
||||
.optional()
|
||||
});
|
||||
|
||||
const formSchemaWithSlug = baseFormSchema.extend({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, "Required")
|
||||
.max(64, "Too long, maximum length is 64 characters")
|
||||
.regex(
|
||||
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
|
||||
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
|
||||
)
|
||||
});
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type BaseFormData = z.infer<typeof baseFormSchema>;
|
||||
type FormDataWithSlug = z.infer<typeof formSchemaWithSlug>;
|
||||
|
||||
type Props = {
|
||||
showSlugField?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isPending } = useUpdateProject();
|
||||
const { handleSubmit, control, reset, watch } = useForm<BaseFormData | FormDataWithSlug>({
|
||||
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
|
||||
});
|
||||
|
||||
const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug;
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? "",
|
||||
...(showSlugField && { slug: currentWorkspace.slug })
|
||||
description: currentWorkspace.description ?? ""
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace, showSlugField]);
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: data.name,
|
||||
newProjectDescription: data.description,
|
||||
...(showSlugField &&
|
||||
"slug" in data && {
|
||||
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
|
||||
})
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -87,34 +65,20 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentSlug || "");
|
||||
createNotification({
|
||||
text: "Copied project slug to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title="Click to copy project slug"
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentWorkspace?.id || "");
|
||||
createNotification({
|
||||
text: "Copied project ID to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title="Click to copy project ID"
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</Button>
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -149,38 +113,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
{showSlugField && (
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project slug"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project slug"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="slug"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
@@ -0,0 +1 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
@@ -1 +1,2 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<ProjectOverviewChangeSection />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -0,0 +1,168 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isPending } = useUpdateProject();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? ""
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated project overview",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update project overview",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project name"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project description"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Project description"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
@@ -1 +1,2 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
@@ -11,6 +11,7 @@ import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||
import { ProductSettings } from "../ProductSettings/ProductSettings";
|
||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
@@ -20,6 +21,7 @@ export const OrgTabGroup = () => {
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||
{ name: "Security", key: "tab-org-security", component: OrgAuthTab },
|
||||
{ name: "Products", key: "tab-org-products", component: ProductSettings },
|
||||
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
|
||||
{
|
||||
name: "Workflow Integrations",
|
||||
|
@@ -0,0 +1,69 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { ContentLoader, Switch } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const ProductConfigSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
|
||||
const handleDisplayAllMembersInviteToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
displayAllMembersInvite: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} display all members invite`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update setting",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentOrg) {
|
||||
return <ContentLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<h3 className="text-md text-mineshaft-100">
|
||||
Display All Members Invite on Project Creation
|
||||
</h3>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="display-all-members-invite"
|
||||
onCheckedChange={(value) => handleDisplayAllMembersInviteToggle(value)}
|
||||
isChecked={currentOrg?.displayAllMembersInvite ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Control whether to display all members in the invite section on project creation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProductSettings = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProductConfigSection />
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -5,19 +5,15 @@ import {
|
||||
faArrowRotateRight,
|
||||
faCheckCircle,
|
||||
faClock,
|
||||
faCopy,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
faServer,
|
||||
faShare,
|
||||
faTag,
|
||||
faTrash,
|
||||
faUser
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -50,7 +46,6 @@ import {
|
||||
} from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -125,7 +120,6 @@ export const SecretDetailSidebar = ({
|
||||
{}
|
||||
);
|
||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const cannotEditSecret = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -198,73 +192,15 @@ export const SecretDetailSidebar = ({
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
};
|
||||
|
||||
const handleReminderSubmit = async (
|
||||
reminderRepeatDays: number | null | undefined,
|
||||
reminderNote: string | null | undefined
|
||||
) => {
|
||||
await onSaveSecret(
|
||||
secret,
|
||||
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
|
||||
() => {}
|
||||
);
|
||||
};
|
||||
const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => {
|
||||
await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { });
|
||||
}
|
||||
|
||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||
|
||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||
const secretReminderNote = watch("reminderNote");
|
||||
|
||||
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||
switch (userType) {
|
||||
case ActorType.USER:
|
||||
return faUser;
|
||||
case ActorType.IDENTITY:
|
||||
return faDesktop;
|
||||
default:
|
||||
return faServer;
|
||||
}
|
||||
};
|
||||
|
||||
const getModifiedByName = (
|
||||
userType: string | undefined | null,
|
||||
userName: string | null | undefined
|
||||
) => {
|
||||
switch (userType) {
|
||||
case ActorType.PLATFORM:
|
||||
return "System-generated";
|
||||
default:
|
||||
return userName;
|
||||
}
|
||||
};
|
||||
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
) => {
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateReminderForm
|
||||
@@ -277,7 +213,7 @@ export const SecretDetailSidebar = ({
|
||||
if (data) {
|
||||
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
||||
setValue("reminderNote", data.note, { shouldDirty: false });
|
||||
handleReminderSubmit(data.days, data.note);
|
||||
handleReminderSubmit(data.days, data.note)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -682,7 +618,7 @@ export const SecretDetailSidebar = ({
|
||||
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||
<div className="mb-2 pl-1">Version History</div>
|
||||
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
|
||||
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
|
||||
<div className="flex flex-row">
|
||||
<div key={id} className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
@@ -697,42 +633,36 @@ export const SecretDetailSidebar = ({
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(
|
||||
actor.actorId,
|
||||
actor.actorType,
|
||||
actor.membershipId
|
||||
)
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={getModifiedByIcon(actor.actorType)}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-left"
|
||||
onClick={(e) => {
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none"
|
||||
onClick={(e) => {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
@@ -750,74 +680,51 @@ export const SecretDetailSidebar = ({
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{secretValue}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{secretValue}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -991,49 +898,29 @@ export const SecretDetailSidebar = ({
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip content="Copy Secret ID">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
ariaLabel="Copy Secret ID"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(secret.id);
|
||||
|
||||
createNotification({
|
||||
title: "Secret ID Copied",
|
||||
text: "The secret ID has been copied to your clipboard.",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
colorSchema="danger"
|
||||
ariaLabel="Delete Secret"
|
||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={onDeleteSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip content="Delete Secret">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
ariaLabel="Delete Secret"
|
||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={onDeleteSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -238,12 +238,10 @@ export const SecretListView = ({
|
||||
if (!isReminderEvent) {
|
||||
handlePopUpClose("secretDetail");
|
||||
}
|
||||
|
||||
|
||||
let successMessage;
|
||||
if (isReminderEvent) {
|
||||
successMessage = reminderRepeatDays
|
||||
? "Successfully saved secret reminder"
|
||||
: "Successfully deleted secret reminder";
|
||||
successMessage = reminderRepeatDays ? "Successfully saved secret reminder" : "Successfully deleted secret reminder";
|
||||
} else {
|
||||
successMessage = "Successfully saved secrets";
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
@@ -8,6 +7,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { EnvironmentSection } from "../EnvironmentSection";
|
||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||
import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ProjectGeneralTab = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<ProjectOverviewChangeSection />
|
||||
{isSecretManager && <EnvironmentSection />}
|
||||
{isSecretManager && <SecretTagsSection />}
|
||||
{isSecretManager && <AutoCapitalizationSection />}
|
||||
|
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -0,0 +1,168 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isPending } = useUpdateProject();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? ""
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated project overview",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update project overview",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project name"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project description"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Project description"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
@@ -2,4 +2,5 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { EnvironmentSection } from "./EnvironmentSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<ProjectOverviewChangeSection />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -0,0 +1,168 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isPending } = useUpdateProject();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? ""
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated project overview",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update project overview",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project name"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project description"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Project description"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
@@ -1 +1,2 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
Reference in New Issue
Block a user