This commit is contained in:
Tuan Dang
2023-06-17 10:35:49 +07:00
22 changed files with 795 additions and 473 deletions

View File

@@ -11,6 +11,7 @@ import {
validateFolderName, validateFolderName,
generateFolderId, generateFolderId,
getParentFromFolderId, getParentFromFolderId,
getFolderByPath,
} from "../../services/FolderService"; } from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables"; import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership"; import { validateMembership } from "../../helpers/membership";
@@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => {
// TODO: validate workspace // TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => { export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as { const { workspaceId, environment, parentFolderId, parentFolderPath } =
workspaceId: string; req.query as {
environment: string; workspaceId: string;
parentFolderId?: string; environment: string;
}; parentFolderId?: string;
parentFolderPath?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
@@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => {
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
}); });
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }],
});
}
if (!parentFolderId) { if (!parentFolderId) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({ const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id, id,

View File

@@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => {
(!folders && folderId && folderId !== "root") || (!folders && folderId && folderId !== "root") ||
(!folders && secretPath) (!folders && secretPath)
) { ) {
throw BadRequestError({ message: "Folder not found" }); res.send({ secrets: [] });
return;
} }
if (folders && folderId !== "root") { if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string); const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) {
res.send({ secrets: [] });
return;
}
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
@@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
if (folders && secretPath) { if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" }); // avoid throwing error and send empty list
const folder = getFolderByPath(folders.nodes, secretPath as string); const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) { if (!folder) {
throw BadRequestError({ message: "Secret path not found" }); res.send({ secrets: [] });
return;
} }
folderId = folder.id; folderId = folder.id;
} }

View File

@@ -34,7 +34,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
secret, secret,
key key
}); });
return rep; return rep;
}) })
}); });
@@ -88,7 +88,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretComment, secretComment,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
@@ -102,12 +102,12 @@ export const createSecretRaw = async (req: Request, res: Response) => {
plaintext: secretValue, plaintext: secretValue,
key key
}); });
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment, plaintext: secretComment,
key key
}); });
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -135,7 +135,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex; delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex,
@@ -202,11 +202,11 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
@@ -391,11 +391,11 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath = "/" secretPath = "/"
} = req.body; } = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({

View File

@@ -1,16 +1,16 @@
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo'); // const MongoStore = require('rate-limit-mongo');
// 200 per minute // 200 per minute
export const apiLimiter = rateLimit({ export const apiLimiter = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60, // expireTimeMs: 1000 * 60,
collectionName: "expressRateRecords-apiLimiter", // collectionName: "expressRateRecords-apiLimiter",
errorHandler: console.error.bind(null, 'rate-limit-mongo') // errorHandler: console.error.bind(null, 'rate-limit-mongo')
}), // }),
windowMs: 1000 * 60, windowMs: 60 * 1000,
max: 200, max: 240,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
skip: (request) => { skip: (request) => {
@@ -23,14 +23,14 @@ export const apiLimiter = rateLimit({
// 50 requests per 1 hours // 50 requests per 1 hours
const authLimit = rateLimit({ const authLimit = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60, // expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'), // errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-authLimit", // collectionName: "expressRateRecords-authLimit",
}), // }),
windowMs: 1000 * 60 * 60, windowMs: 60 * 1000,
max: 50, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {
@@ -40,14 +40,14 @@ const authLimit = rateLimit({
// 5 requests per 1 hour // 5 requests per 1 hour
export const passwordLimiter = rateLimit({ export const passwordLimiter = rateLimit({
store: new MongoStore({ // store: new MongoStore({
uri: process.env.MONGO_URL, // uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60, // expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'), // errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-passwordLimiter", // collectionName: "expressRateRecords-passwordLimiter",
}), // }),
windowMs: 1000 * 60 * 60, windowMs: 60 * 60 * 1000,
max: 5, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {

View File

@@ -57,7 +57,7 @@ import { getFolderIdFromServiceToken } from "../services/FolderService";
export const repackageSecretToRaw = ({ export const repackageSecretToRaw = ({
secret, secret,
key key
}:{ }: {
secret: ISecret; secret: ISecret;
key: string; key: string;
}) => { }) => {
@@ -76,8 +76,8 @@ export const repackageSecretToRaw = ({
key key
}); });
let secretComment: string = ''; let secretComment: string = '';
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
secretComment = decryptSymmetric128BitHexKeyUTF8({ secretComment = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext, ciphertext: secret.secretCommentCiphertext,
@@ -86,7 +86,7 @@ export const repackageSecretToRaw = ({
key key
}); });
} }
return ({ return ({
_id: secret._id, _id: secret._id,
version: secret.version, version: secret.version,
@@ -503,7 +503,7 @@ export const getSecretsHelper = async ({
folder: folderId, folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}).lean(); }).populate("tags").lean();
// concat with shared secrets // concat with shared secrets
secrets = secrets.concat( secrets = secrets.concat(
@@ -515,7 +515,7 @@ export const getSecretsHelper = async ({
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex),
}, },
}).lean() }).populate("tags").lean()
); );
// (EE) create (audit) log // (EE) create (audit) log
@@ -553,7 +553,7 @@ export const getSecretsHelper = async ({
}, },
}); });
} }
return secrets; return secrets;
}; };
@@ -652,7 +652,7 @@ export const getSecretHelper = async ({
}, },
}); });
} }
return secret; return secret;
}; };
@@ -843,7 +843,7 @@ export const deleteSecretHelper = async ({
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) { if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -909,12 +909,12 @@ export const deleteSecretHelper = async ({
}); });
action && (await EELogService.createLog({ action && (await EELogService.createLog({
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP,
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
@@ -941,7 +941,7 @@ export const deleteSecretHelper = async ({
}, },
}); });
} }
return ({ return ({
secrets, secrets,
secret secret

View File

@@ -63,6 +63,7 @@ router.get(
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("parentFolderId").optional().isString().trim(), query("parentFolderId").optional().isString().trim(),
query("parentFolderPath").optional().isString().trim(),
validateRequest, validateRequest,
getFolders getFolders
); );

View File

@@ -11,63 +11,6 @@ import (
const USER_AGENT = "cli" const USER_AGENT = "cli"
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) { func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId) endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse var result GetEncryptedWorkspaceKeyResponse
@@ -107,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil return tokenDetailsResponse, nil
} }
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var secretsResponse GetEncryptedSecretsV2Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
return secretsResponse, nil
}
func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) { func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) {
var loginOneV2Response GetLoginOneV2Response var loginOneV2Response GetLoginOneV2Response
response, err := httpClient. response, err := httpClient.
@@ -303,3 +224,110 @@ func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToke
return newAccessToken, nil return newAccessToken, nil
} }
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return secretsResponse, nil
}
func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallDeleteSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}

View File

@@ -143,24 +143,7 @@ type Secret struct {
SecretCommentHash string `json:"secretCommentHash,omitempty"` SecretCommentHash string `json:"secretCommentHash,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
} PlainTextKey string `json:"plainTextKey"`
type BatchCreateSecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchModifySecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchDeleteSecretsBySecretIdsRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
SecretIds []string `json:"secretIds"`
} }
type GetEncryptedWorkspaceKeyRequest struct { type GetEncryptedWorkspaceKeyRequest struct {
@@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
} }
type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
}
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
} `json:"secrets"`
}
type GetServiceTokenDetailsResponse struct { type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -320,3 +268,109 @@ type VerifyMfaTokenErrorResponse struct {
type GetNewAccessTokenWithRefreshTokenResponse struct { type GetNewAccessTokenWithRefreshTokenResponse struct {
Token string `json:"token"` Token string `json:"token"`
} }
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
}
type GetEncryptedSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}
type CreateSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretPath string `json:"secretPath"`
}
type DeleteSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
}
type GetSingleSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type GetSingleSecretByNameSecretResponse struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}

View File

@@ -82,7 +82,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -184,6 +189,7 @@ func init() {
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
runCmd.Flags().String("path", "/", "get secrets within a folder path")
} }
// Will execute a single command and pass in the given secrets into the process // Will execute a single command and pass in the given secrets into the process

View File

@@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
@@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -103,6 +108,11 @@ var secretsSetCmd = &cobra.Command{
} }
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceFile, err := util.GetWorkSpaceFromFile() workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil { if err != nil {
util.HandleError(err, "Unable to get your local config details") util.HandleError(err, "Unable to get your local config details")
@@ -140,7 +150,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets // pull current secrets
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath})
if err != nil { if err != nil {
util.HandleError(err, "unable to retrieve secrets") util.HandleError(err, "unable to retrieve secrets")
} }
@@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue, SecretValueHash: hashedValue,
PlainTextKey: key,
Type: existingSecret.Type,
} }
// Only add to modifications if the value is different // Only add to modifications if the value is different
@@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue, SecretValueHash: hashedValue,
Type: util.SECRET_TYPE_SHARED, Type: util.SECRET_TYPE_SHARED,
PlainTextKey: key,
} }
secretsToCreate = append(secretsToCreate, encryptedSecretDetails) secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{ secretOperations = append(secretOperations, SecretSetOperation{
@@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{
} }
} }
if len(secretsToCreate) > 0 { for _, secret := range secretsToCreate {
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{ createSecretRequest := api.CreateSecretV3Request{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
Secrets: secretsToCreate, SecretName: secret.PlainTextKey,
SecretKeyCiphertext: secret.SecretKeyCiphertext,
SecretKeyIV: secret.SecretKeyIV,
SecretKeyTag: secret.SecretKeyTag,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
} }
err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) err = api.CallCreateSecretsV3(httpClient, createSecretRequest)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process new secret creations") util.HandleError(err, "Unable to process new secret creations")
return return
} }
} }
if len(secretsToModify) > 0 { for _, secret := range secretsToModify {
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{ updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
Secrets: secretsToModify, SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
} }
err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process the modifications to your secrets") util.HandleError(err, "Unable to process secret update request")
return return
} }
} }
@@ -288,6 +314,16 @@ var secretsDeleteCmd = &cobra.Command{
} }
} }
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretType, err := cmd.Flags().GetString("type")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
util.HandleError(err, "Unable to authenticate") util.HandleError(err, "Unable to authenticate")
@@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details") util.HandleError(err, "Unable to get local project details")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) for _, secretName := range args {
if err != nil { request := api.DeleteSecretV3Request{
util.HandleError(err, "Unable to fetch secrets") WorkspaceId: workspaceFile.WorkspaceId,
} Environment: environmentName,
SecretName: secretName,
secretByKey := getSecretsByKeys(secrets) Type: secretType,
validSecretIdsToDelete := []string{} SecretPath: secretsPath,
invalidSecretNamesThatDoNotExist := []string{}
for _, secretKeyFromArg := range args {
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
} else {
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
} }
}
if len(invalidSecretNamesThatDoNotExist) != 0 { httpClient := resty.New().
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
util.PrintErrorMessageAndExit(message) SetHeader("Accept", "application/json")
}
request := api.BatchDeleteSecretsBySecretIdsRequest{ err = api.CallDeleteSecretsV3(httpClient, request)
WorkspaceId: workspaceFile.WorkspaceId, if err != nil {
EnvironmentName: environmentName, util.HandleError(err, "Unable to complete your delete request")
SecretIds: validSecretIdsToDelete, }
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your batch delete request")
} }
fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", ")) fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION))
}, },
} }
@@ -611,11 +629,15 @@ func init() {
secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin() util.RequireLogin()
util.RequireLocalWorkspaceFile() util.RequireLocalWorkspaceFile()
} }
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsCmd.AddCommand(secretsDeleteCmd) secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin() util.RequireLogin()
@@ -626,5 +648,6 @@ func init() {
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
} }

View File

@@ -64,4 +64,5 @@ type GetAllSecretsParameters struct {
InfisicalToken string InfisicalToken string
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string
} }

View File

@@ -34,7 +34,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
}) })
@@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return plainTextSecrets, serviceTokenDetails, nil return plainTextSecrets, serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
}) }
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -162,7 +168,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
} }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs) secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn return secretsToReturn
} }
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{} plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets { for _, secret := range encryptedSecrets.Secrets {
// Decrypt key // Decrypt key

View File

@@ -1 +1,7 @@
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries'; export {
useCreateFolder,
useDeleteFolder,
useGetProjectFolders,
useGetProjectFoldersBatch,
useUpdateFolder
} from './queries';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request'; import { apiRequest } from '@app/config/request';
@@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries';
import { import {
CreateFolderDTO, CreateFolderDTO,
DeleteFolderDTO, DeleteFolderDTO,
GetProjectFoldersBatchDTO,
GetProjectFoldersDTO, GetProjectFoldersDTO,
TSecretFolder, TSecretFolder,
UpdateFolderDTO UpdateFolderDTO
@@ -17,6 +18,26 @@ const queryKeys = {
['secret-folders', { workspaceId, environment, parentFolderId }] as const ['secret-folders', { workspaceId, environment, parentFolderId }] as const
}; };
const fetchProjectFolders = async (
workspaceId: string,
environment: string,
parentFolderId?: string,
parentFolderPath?: string
) => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId,
parentFolderPath
}
}
);
return data;
};
export const useGetProjectFolders = ({ export const useGetProjectFolders = ({
workspaceId, workspaceId,
parentFolderId, parentFolderId,
@@ -27,19 +48,7 @@ export const useGetProjectFolders = ({
useQuery({ useQuery({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
queryFn: async () => { queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId),
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId
}
}
);
return data;
},
select: useCallback( select: useCallback(
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ ({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
dir, dir,
@@ -53,6 +62,25 @@ export const useGetProjectFolders = ({
) )
}); });
export const useGetProjectFoldersBatch = ({
folders = [],
isPaused,
parentFolderPath
}: GetProjectFoldersBatchDTO) =>
useQueries({
queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath),
queryFn: async () =>
fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
environment,
folders: data.folders,
dir: data.dir
})
}))
});
export const useCreateFolder = () => { export const useCreateFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = {
sortDir?: 'asc' | 'desc'; sortDir?: 'asc' | 'desc';
}; };
export type GetProjectFoldersBatchDTO = {
folders: Omit<GetProjectFoldersDTO, 'isPaused' | 'sortDir'>[];
isPaused?: boolean;
parentFolderPath?: string;
};
export type CreateFolderDTO = { export type CreateFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { import {
@@ -29,14 +30,16 @@ export const secretKeys = {
const fetchProjectEncryptedSecrets = async ( const fetchProjectEncryptedSecrets = async (
workspaceId: string, workspaceId: string,
env: string | string[], env: string | string[],
folderId?: string folderId?: string,
secretPath?: string
) => { ) => {
if (typeof env === 'string') { if (typeof env === 'string') {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: { params: {
environment: env, environment: env,
workspaceId, workspaceId,
folderId: folderId || undefined folderId: folderId || undefined,
secretPath
} }
}); });
return data.secrets; return data.secrets;
@@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async (
params: { params: {
environment: envPoint, environment: envPoint,
workspaceId, workspaceId,
folderId folderId,
secretPath
} }
}); });
allEnvData = allEnvData.concat(data.secrets); allEnvData = allEnvData.concat(data.secrets);
@@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: (data) => { select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({
} }
}); });
return { secrets: sharedSecrets }; return { secrets: sharedSecrets };
} }, [decryptFileKey])
}); });
export const useGetProjectSecretsByKey = ({ export const useGetProjectSecretsByKey = ({
workspaceId, workspaceId,
env, env,
decryptFileKey, decryptFileKey,
isPaused isPaused,
folderId,
secretPath
}: GetProjectSecretsDTO) => }: GetProjectSecretsDTO) =>
useQuery({ useQuery({
// wait for all values to be available // wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env), // right now secretpath is passed as folderid as only this is used in overview
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
select: (data) => { queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({
}); });
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
} }, [decryptFileKey])
}); });
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey), enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId), queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: (data) => { select: useCallback((data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = dto.decryptFileKey; const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
}) })
})) }))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
} }, [])
}); });
export const useBatchSecretsOp = () => { export const useBatchSecretsOp = () => {

View File

@@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
env: string | string[]; env: string | string[];
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
folderId?: string; folderId?: string;
secretPath?: string;
isPaused?: boolean; isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void; onSuccess?: (data: DecryptedSecret[]) => void;
}; };

View File

@@ -12,13 +12,6 @@ const Dashboard = () => {
const queryEnv = router.query.env as string; const queryEnv = router.query.env as string;
const isOverviewMode = !queryEnv; const isOverviewMode = !queryEnv;
const onExploreEnv = (slug: string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, env: slug }
});
};
return ( return (
<> <>
<Head> <Head>
@@ -29,11 +22,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t('dashboard.og-description'))} /> <meta name="og:description" content={String(t('dashboard.og-description'))} />
</Head> </Head>
<div className="h-full"> <div className="h-full">
{isOverviewMode ? ( {isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
<DashboardEnvOverview onEnvChange={onExploreEnv} />
) : (
<DashboardPage envFromTop={queryEnv} />
)}
</div> </div>
</> </>
); );

View File

@@ -4,20 +4,20 @@
@layer utilities { @layer utilities {
.flex-0 { .flex-0 {
flex:0; flex: 0;
} }
.flex-2 { .flex-2 {
flex-grow: 2; flex-grow: 2;
} }
.flex-3 { .flex-3 {
flex-grow: 3; flex-grow: 3;
} }
} }
@layer components { @layer components {
.secret-table { .secret-table {
@apply bg-mineshaft-800 text-left text-bunker-300 w-full; @apply w-full bg-mineshaft-800 text-left text-bunker-300;
} }
/* padding except for comment column */ /* padding except for comment column */
@@ -29,13 +29,45 @@
@apply py-1 px-1 pr-2 text-sm; @apply py-1 px-1 pr-2 text-sm;
} }
.secret-table th:not(:last-child),.secret-table td:not(:last-child) { .secret-table th:not(:last-child),
.secret-table td:not(:last-child) {
@apply border-r border-mineshaft-600; @apply border-r border-mineshaft-600;
} }
.secret-table tr { .secret-table tr {
@apply border-b border-mineshaft-600; @apply border-b border-mineshaft-600;
} }
.breadcrumb::after,
.breadcrumb::before {
content: '';
height: 60%;
width: 100%;
z-index: -1;
display: block;
position: absolute;
@apply bg-mineshaft-800;
}
.breadcrumb:hover::before {
@apply bg-mineshaft-600;
}
.breadcrumb:hover::after {
@apply bg-mineshaft-600;
}
.breadcrumb::after {
left: 5px;
bottom: -3px;
transform: skew(-30deg);
}
.breadcrumb::before {
left: 5px;
top: -3px;
transform: skew(30deg);
}
} }
@import '@fontsource/inter/400.css'; @import '@fontsource/inter/400.css';

View File

@@ -1,35 +1,31 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2'; import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
import { useWorkspace } from '@app/context'; import { useWorkspace } from '@app/context';
import { import {
useGetProjectFoldersBatch,
useGetProjectSecretsByKey, useGetProjectSecretsByKey,
useGetUserWsEnvironments, useGetUserWsEnvironments,
useGetUserWsKey useGetUserWsKey
} from '@app/hooks/api'; } from '@app/hooks/api';
import { WorkspaceEnv } from '@app/hooks/api/types';
import { EnvComparisonRow } from './components/EnvComparisonRow'; import { EnvComparisonRow } from './components/EnvComparisonRow';
import { FormData, schema } from './DashboardPage.utils'; import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow';
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { export const DashboardEnvOverview = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const secretPath = router.query?.secretPath as string;
useEffect(() => { useEffect(() => {
if (!isLoading && !workspaceId && router.isReady) { if (!isLoading && !workspaceId && router.isReady) {
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
}, [isLoading, workspaceId, router.isReady]); }, [isLoading, workspaceId, router.isReady]);
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
workspaceId, workspaceId
onSuccess: (data) => {
// get an env with one of the access available
const env = data.find(({ isReadDenied }) => !isReadDenied);
if (env) {
setSelectedEnv(env);
}
}
}); });
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied); const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
workspaceId, workspaceId,
env: userAvailableEnvs?.map((env) => env.slug) ?? [], env: userAvailableEnvs?.map((env) => env.slug) ?? [],
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: false isPaused: false,
secretPath
}); });
const method = useForm<FormData>({ const folders = useGetProjectFoldersBatch({
// why any: well yup inferred ts expects other keys to defined as undefined folders:
defaultValues: secrets as any, userAvailableEnvs?.map((env) => ({
values: secrets as any, environment: env.slug,
mode: 'onBlur', workspaceId
resolver: yupResolver(schema) })) ?? [],
parentFolderPath: secretPath
}); });
const foldersGroupedByEnv = useMemo(() => {
const res: Record<string, Record<string, boolean>> = {};
folders.forEach(({ data }) => {
data?.folders
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
?.forEach((folder) => {
if (!res?.[folder.name]) res[folder.name] = {};
res[folder.name][data.environment] = true;
});
});
return res;
}, [folders, userAvailableEnvs, searchFilter]);
const numSecretsMissingPerEnv = useMemo(() => { const numSecretsMissingPerEnv = useMemo(() => {
// first get all sec in the env then subtract with total to get missing ones // first get all sec in the env then subtract with total to get missing ones
const secPerEnvMissing: Record<string, number> = Object.fromEntries( const secPerEnvMissing: Record<string, number> = Object.fromEntries(
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
return secPerEnvMissing; return secPerEnvMissing;
}, [secrets, userAvailableEnvs]); }, [secrets, userAvailableEnvs]);
const isReadOnly = selectedEnv?.isWriteDenied; const onExploreEnv = (slug: string) => {
const query: Record<string, string> = { ...router.query, env: slug };
delete query.secretPath;
// the dir return will have the present directory folder id
// use that when clicking on explore to redirect user to there
const envFolder = folders.find(({ data }) => slug === data?.environment);
const dir = envFolder?.data?.dir?.pop();
if (dir) {
query.folderId = dir.id;
}
router.push({
pathname: router.pathname,
query
});
};
const onFolderClick = (path: string) => {
router.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ''}/${path}`
}
});
};
const onFolderCrumbClick = (index: number) => {
const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/');
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
// root condition
if (index === 0) delete query.secretPath;
router.push({
pathname: router.pathname,
query
});
};
if (isSecretsLoading || isEnvListLoading) { if (isSecretsLoading || isEnvListLoading) {
return ( return (
@@ -91,165 +131,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
); );
} }
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
);
// when secrets is not loading and secrets list is empty // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length; const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
const isFoldersEmtpy =
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
!Object.keys(foldersGroupedByEnv).length;
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
return ( return (
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}> <div className="relative right-5">
<form autoComplete="off"> <NavHeader pageName={t('dashboard.title')} isProjectRelated />
{/* breadcrumb row */} </div>
<div className="relative right-5"> <div className="mt-6">
<NavHeader pageName={t('dashboard.title')} isProjectRelated /> <p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
</p>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
</div> </div>
<div className="mt-6"> {(secretPath || '')
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p> .split('/')
<p className="text-md text-bunker-300"> .filter(Boolean)
Inject your secrets using .map((path, index, arr) => (
<a <div
className="mx-1 text-primary/80 hover:text-primary" key={`secret-path-${index + 1}`}
href="https://infisical.com/docs/cli/overview" className={`breadcrumb relative z-20 ${
target="_blank" index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
rel="noopener noreferrer" } border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={() => null}
role="button"
tabIndex={0}
> >
Infisical CLI {path}
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
</p>
</div>
<div className="absolute top-[11.1rem] right-6 flex w-full max-w-sm flex-grow space-x-2">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by secret name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
<div className="overflow-y-auto">
<div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">{0}</div>
</div> </div>
<div className="sticky top-0 border-none"> ))}
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]"> </div>
<div className="text-sm font-medium ">Secret</div> <div className="w-80">
</div> <Input
</div> className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
{numSecretsMissingPerEnv && placeholder="Search by secret/folder name..."
userAvailableEnvs?.map((env) => { value={searchFilter}
return ( onChange={(e) => setSearchFilter(e.target.value)}
<div leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
key={`header-${env.slug}`} />
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800" </div>
> </div>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]"> <div className="overflow-y-auto">
{env.name} <div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
{numSecretsMissingPerEnv[env.slug] > 0 && ( <div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100"> <div className="w-10 text-center text-xs text-transparent">{0}</div>
<Tooltip </div>
content={`${ <div className="sticky top-0 border-none">
numSecretsMissingPerEnv[env.slug] <div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
} secrets missing compared to other environments`} <div className="text-sm font-medium ">Secret</div>
> </div>
<span className="text-bunker-100"> </div>
{numSecretsMissingPerEnv[env.slug]} {numSecretsMissingPerEnv &&
</span> userAvailableEnvs?.map((env) => {
</Tooltip> return (
</div> <div
)} key={`header-${env.slug}`}
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
{env.name}
{numSecretsMissingPerEnv[env.slug] > 0 && (
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
<Tooltip
content={`${
numSecretsMissingPerEnv[env.slug]
} secrets missing compared to other environments`}
>
<span className="text-bunker-100">
{numSecretsMissingPerEnv[env.slug]}
</span>
</Tooltip>
</div> </div>
</div> )}
);
})}
</div>
<div
className={`${
isDashboardSecretEmpty ? '' : ''
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
>
{!isDashboardSecretEmpty && (
<TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto">
{Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly={isReadOnly}
index={index}
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{isDashboardSecretEmpty && (
<div className="flex h-40 w-full flex-row rounded-md">
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
<FontAwesomeIcon icon={faKey} className="text-4xl mb-4" />
<span className="mb-1">No secrets found.</span>
<span>To add more secrets you can explore any environment.</span>
</div> </div>
</div> </div>
)} );
{/* In future, we should add an option to add environments here })}
<div className="flex items-start justify-center h-full ml-10"> </div>
<Button <div
leftIcon={<FontAwesomeIcon icon={faPlus}/>} className={`${
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })} isDashboardEmpty ? '' : ''
variant="outline_bg" } no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
colorSchema="primary" >
isFullWidth {!isDashboardEmpty && (
className="h-10" <TableContainer className="border-none">
> <table className="secret-table relative w-full bg-mineshaft-900">
Add Environment <tbody className="max-h-screen overflow-y-auto">
</Button> {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
</div> */} <FolderComparisonRow
</div> key={`${folderName}-${index + 1}`}
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center"> folderName={folderName}
<div className="flex h-10 w-10 items-center justify-center border-none px-4"> userAvailableEnvs={userAvailableEnvs}
<div className="w-10 text-center text-xs text-transparent">0</div> folderInEnv={foldersGroupedByEnv[folderName]}
onClick={onFolderClick}
/>
))}
{Object.keys(secrets?.secrets || {})
?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
)
.map((key) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{isDashboardEmpty && (
<div className="flex h-40 w-full flex-row rounded-md">
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
<span className="mb-1">No secrets/folders found.</span>
<span>To add more secrets you can explore any environment.</span>
</div> </div>
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onEnvChange(env.slug)}
// router.push(`${router.asPath }?env=${env.slug}`)
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div> </div>
)}
</div>
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">0</div>
</div> </div>
</form> <div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
</FormProvider> <span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onExploreEnv(env.slug)}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -1,11 +1,10 @@
/* eslint-disable react/jsx-no-useless-fragment */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
type Props = { type Props = {
index: number;
secrets: any[] | undefined; secrets: any[] | undefined;
// permission and external state's that decided to hide or show // permission and external state's that decided to hide or show
isReadOnly?: boolean; isReadOnly?: boolean;
@@ -30,7 +29,7 @@ const DashboardInput = ({
if (val === undefined) if (val === undefined)
return ( return (
<span className="cursor-default font-sans text-xs italic text-red-500/80"> <span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" /> <FontAwesomeIcon icon={faMinus} className="mt-1" />
</span> </span>
); );
if (val?.length === 0) if (val?.length === 0)
@@ -110,7 +109,6 @@ const DashboardInput = ({
}; };
export const EnvComparisonRow = ({ export const EnvComparisonRow = ({
index,
secrets, secrets,
isSecretValueHidden, isSecretValueHidden,
isReadOnly, isReadOnly,
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
return ( return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800"> <tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<td className="flex h-10 w-10 items-center justify-center border-none px-4"> <td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> <div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faKey} />
</div>
</td> </td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]"> <td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate"> <div className="flex h-8 cursor-default flex-row items-center truncate">

View File

@@ -0,0 +1,42 @@
import { faCheck, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type Props = {
folderInEnv: Record<string, boolean>;
userAvailableEnvs?: Array<{ slug: string; name: string }>;
folderName: string;
onClick: (folderName: string) => void;
};
export const FolderComparisonRow = ({
folderInEnv = {},
userAvailableEnvs = [],
folderName,
onClick
}: Props) => (
<tr
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
onClick={() => onClick(folderName)}
>
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
</td>
{userAvailableEnvs?.map(({ slug }) => (
<td
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
folderInEnv[slug]
? 'bg-mineshaft-900/30 text-green-500/80'
: 'bg-red-800/10 text-red-500/80'
}`}
key={`${folderName}-${slug}`}
>
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
</td>
))}
</tr>
);