mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 08:27:53 +00:00
feat(folder-scoped-st): added batch,create secrets v2 secretpath support and service token
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { ISecret, Secret } from "../../models";
|
import { ISecret, Secret, ServiceTokenData } from "../../models";
|
||||||
import { IAction, SecretVersion } from "../../ee/models";
|
import { IAction, SecretVersion } from "../../ee/models";
|
||||||
import {
|
import {
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
@@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
|
|||||||
import Folder from "../../models/folder";
|
import Folder from "../../models/folder";
|
||||||
import {
|
import {
|
||||||
getFolderByPath,
|
getFolderByPath,
|
||||||
|
getFolderIdFromServiceToken,
|
||||||
searchByFolderId,
|
searchByFolderId,
|
||||||
} from "../../services/FolderService";
|
} from "../../services/FolderService";
|
||||||
|
|
||||||
@@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
|
||||||
requests,
|
requests,
|
||||||
|
secretPath,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
folderId: string;
|
|
||||||
requests: BatchSecretRequest[];
|
requests: BatchSecretRequest[];
|
||||||
|
secretPath: string;
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
let folderId = req.body.folderId as string;
|
||||||
|
|
||||||
const createSecrets: BatchSecret[] = [];
|
const createSecrets: BatchSecret[] = [];
|
||||||
const updateSecrets: BatchSecret[] = [];
|
const updateSecrets: BatchSecret[] = [];
|
||||||
@@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||||
|
// in service token when not giving secretpath folderid must be root
|
||||||
|
// this is to avoid giving folderid when service tokens are used
|
||||||
|
if (
|
||||||
|
(!secretPath && folderId !== "root") ||
|
||||||
|
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (secretPath) {
|
||||||
|
folderId = await getFolderIdFromServiceToken(
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for await (const request of requests) {
|
for await (const request of requests) {
|
||||||
// do a validation
|
// do a validation
|
||||||
|
|
||||||
@@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
numberOfSecrets: createdSecrets.length,
|
numberOfSecrets: createdSecrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
folderId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"],
|
||||||
},
|
},
|
||||||
@@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags: u.tags,
|
tags: u.tags,
|
||||||
folder: u.folder
|
folder: u.folder,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
numberOfSecrets: updateSecrets.length,
|
numberOfSecrets: updateSecrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
folderId,
|
||||||
channel,
|
channel,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"],
|
||||||
},
|
},
|
||||||
@@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
secretPath,
|
||||||
}: { workspaceId: string; environment: string; folderId: string } = req.body;
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath?: string;
|
||||||
|
} = req.body;
|
||||||
|
let folderId = req.body.folderId;
|
||||||
|
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
const hasAccess = await userHasWorkspaceAccess(
|
const hasAccess = await userHasWorkspaceAccess(
|
||||||
@@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
// case: create 1 secret
|
// case: create 1 secret
|
||||||
listOfSecretsToCreate = [req.body.secrets];
|
listOfSecretsToCreate = [req.body.secrets];
|
||||||
}
|
}
|
||||||
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||||
|
// in service token when not giving secretpath folderid must be root
|
||||||
|
// this is to avoid giving folderid when service tokens are used
|
||||||
|
if (
|
||||||
|
(!secretPath && folderId !== "root") ||
|
||||||
|
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (secretPath) {
|
||||||
|
folderId = await getFolderIdFromServiceToken(
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// get secret blind index salt
|
// get secret blind index salt
|
||||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||||
@@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
folderId,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||||
|
// in service token when not giving secretpath folderid must be root
|
||||||
|
// this is to avoid giving folderid when service tokens are used
|
||||||
|
if (
|
||||||
|
(!secretPath && folderId !== "root") ||
|
||||||
|
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (folders && secretPath) {
|
if (folders && secretPath) {
|
||||||
if (!folders) throw BadRequestError({ message: "Folder not found" });
|
if (!folders) throw BadRequestError({ message: "Folder not found" });
|
||||||
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
||||||
@@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel,
|
channel,
|
||||||
|
folderId,
|
||||||
userAgent: req.headers?.["user-agent"],
|
userAgent: req.headers?.["user-agent"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
tags,
|
tags,
|
||||||
...(secretCommentCiphertext !== undefined &&
|
...(secretCommentCiphertext !== undefined &&
|
||||||
secretCommentIV &&
|
secretCommentIV &&
|
||||||
secretCommentTag
|
secretCommentTag
|
||||||
? {
|
? {
|
||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,30 +44,7 @@ import {
|
|||||||
getAuthDataPayloadIdObj,
|
getAuthDataPayloadIdObj,
|
||||||
getAuthDataPayloadUserObj,
|
getAuthDataPayloadUserObj,
|
||||||
} from "../utils/auth";
|
} from "../utils/auth";
|
||||||
import Folder from "../models/folder";
|
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||||
import { getFolderByPath } from "../services/FolderService";
|
|
||||||
|
|
||||||
export const getFolderIdFromServiceToken = async (
|
|
||||||
workspaceId: Types.ObjectId | string,
|
|
||||||
environment: string,
|
|
||||||
secretPath: string
|
|
||||||
) => {
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!folders) {
|
|
||||||
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
|
|
||||||
} else {
|
|
||||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
|
||||||
if (!folder) {
|
|
||||||
throw new Error("Folder not found");
|
|
||||||
}
|
|
||||||
return folder.id;
|
|
||||||
}
|
|
||||||
return "root";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret blind index data containing encrypted blind index [salt]
|
* Create secret blind index data containing encrypted blind index [salt]
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import express from 'express';
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
requireSecretsAuth,
|
requireSecretsAuth,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
} from '../../middleware';
|
} from "../../middleware";
|
||||||
import { validateClientForSecrets } from '../../validation';
|
import { validateClientForSecrets } from "../../validation";
|
||||||
import { query, body } from 'express-validator';
|
import { query, body } from "express-validator";
|
||||||
import { secretsController } from '../../controllers/v2';
|
import { secretsController } from "../../controllers/v2";
|
||||||
import {
|
import {
|
||||||
ADMIN,
|
ADMIN,
|
||||||
MEMBER,
|
MEMBER,
|
||||||
@@ -21,11 +21,11 @@ import {
|
|||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY,
|
AUTH_MODE_API_KEY,
|
||||||
} from '../../variables';
|
} from "../../variables";
|
||||||
import { BatchSecretRequest } from '../../types/secret';
|
import { BatchSecretRequest } from "../../types/secret";
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/batch',
|
"/batch",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [
|
acceptedAuthModes: [
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
@@ -35,12 +35,13 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'body',
|
locationWorkspaceId: "body",
|
||||||
}),
|
}),
|
||||||
body('workspaceId').exists().isString().trim(),
|
body("workspaceId").exists().isString().trim(),
|
||||||
body('folderId').default('root').isString().trim(),
|
body("folderId").default("root").isString().trim(),
|
||||||
body('environment').exists().isString().trim(),
|
body("environment").exists().isString().trim(),
|
||||||
body('requests')
|
body("secretPath").optional().isString().trim(),
|
||||||
|
body("requests")
|
||||||
.exists()
|
.exists()
|
||||||
.custom(async (requests: BatchSecretRequest[], { req }) => {
|
.custom(async (requests: BatchSecretRequest[], { req }) => {
|
||||||
if (Array.isArray(requests)) {
|
if (Array.isArray(requests)) {
|
||||||
@@ -65,17 +66,18 @@ router.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
"/",
|
||||||
body('workspaceId').exists().isString().trim(),
|
body("workspaceId").exists().isString().trim(),
|
||||||
body('environment').exists().isString().trim(),
|
body("environment").exists().isString().trim(),
|
||||||
body('folderId').default('root').isString().trim(),
|
body("folderId").default("root").isString().trim(),
|
||||||
body('secrets')
|
body("secretPath").optional().isString().trim(),
|
||||||
|
body("secrets")
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: create multiple secrets
|
// case: create multiple secrets
|
||||||
if (value.length === 0)
|
if (value.length === 0)
|
||||||
throw new Error('secrets cannot be an empty array');
|
throw new Error("secrets cannot be an empty array");
|
||||||
for (const secret of value) {
|
for (const secret of value) {
|
||||||
if (
|
if (
|
||||||
!secret.type ||
|
!secret.type ||
|
||||||
@@ -85,16 +87,16 @@ router.post(
|
|||||||
!secret.secretKeyCiphertext ||
|
!secret.secretKeyCiphertext ||
|
||||||
!secret.secretKeyIV ||
|
!secret.secretKeyIV ||
|
||||||
!secret.secretKeyTag ||
|
!secret.secretKeyTag ||
|
||||||
typeof secret.secretValueCiphertext !== 'string' ||
|
typeof secret.secretValueCiphertext !== "string" ||
|
||||||
!secret.secretValueIV ||
|
!secret.secretValueIV ||
|
||||||
!secret.secretValueTag
|
!secret.secretValueTag
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'secrets array must contain objects that have required secret properties'
|
"secrets array must contain objects that have required secret properties"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === "object") {
|
||||||
// case: update 1 secret
|
// case: update 1 secret
|
||||||
if (
|
if (
|
||||||
!value.type ||
|
!value.type ||
|
||||||
@@ -107,11 +109,11 @@ router.post(
|
|||||||
!value.secretValueTag
|
!value.secretValueTag
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'secrets object is missing required secret properties'
|
"secrets object is missing required secret properties"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('secrets must be an object or an array of objects');
|
throw new Error("secrets must be an object or an array of objects");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -126,19 +128,20 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'body',
|
locationWorkspaceId: "body",
|
||||||
locationEnvironment: 'body',
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.createSecrets
|
secretsController.createSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
"/",
|
||||||
query('workspaceId').exists().trim(),
|
query("workspaceId").exists().trim(),
|
||||||
query('environment').exists().trim(),
|
query("environment").exists().trim(),
|
||||||
query('tagSlugs'),
|
query("tagSlugs"),
|
||||||
query('folderId').default('root').isString().trim(),
|
query("folderId").default("root").isString().trim(),
|
||||||
|
query("secretPath").optional().isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [
|
acceptedAuthModes: [
|
||||||
@@ -150,34 +153,34 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'query',
|
locationWorkspaceId: "query",
|
||||||
locationEnvironment: 'query',
|
locationEnvironment: "query",
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.getSecrets
|
secretsController.getSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/',
|
"/",
|
||||||
body('secrets')
|
body("secrets")
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: update multiple secrets
|
// case: update multiple secrets
|
||||||
if (value.length === 0)
|
if (value.length === 0)
|
||||||
throw new Error('secrets cannot be an empty array');
|
throw new Error("secrets cannot be an empty array");
|
||||||
for (const secret of value) {
|
for (const secret of value) {
|
||||||
if (!secret.id) {
|
if (!secret.id) {
|
||||||
throw new Error('Each secret must contain a ID property');
|
throw new Error("Each secret must contain a ID property");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === "object") {
|
||||||
// case: update 1 secret
|
// case: update 1 secret
|
||||||
if (!value.id) {
|
if (!value.id) {
|
||||||
throw new Error('secret must contain a ID property');
|
throw new Error("secret must contain a ID property");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('secrets must be an object or an array of objects');
|
throw new Error("secrets must be an object or an array of objects");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -198,21 +201,21 @@ router.patch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/',
|
"/",
|
||||||
body('secretIds')
|
body("secretIds")
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
// case: delete 1 secret
|
// case: delete 1 secret
|
||||||
if (typeof value === 'string') return true;
|
if (typeof value === "string") return true;
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: delete multiple secrets
|
// case: delete multiple secrets
|
||||||
if (value.length === 0)
|
if (value.length === 0)
|
||||||
throw new Error('secrets cannot be an empty array');
|
throw new Error("secrets cannot be an empty array");
|
||||||
return value.every((id: string) => typeof id === 'string');
|
return value.every((id: string) => typeof id === "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('secretIds must be a string or an array of strings');
|
throw new Error("secretIds must be a string or an array of strings");
|
||||||
})
|
})
|
||||||
.not()
|
.not()
|
||||||
.isEmpty(),
|
.isEmpty(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { TFolderSchema } from "../models/folder";
|
import { Types } from "mongoose";
|
||||||
|
import Folder, { TFolderSchema } from "../models/folder";
|
||||||
|
|
||||||
type TAppendFolderDTO = {
|
type TAppendFolderDTO = {
|
||||||
folderName: string;
|
folderName: string;
|
||||||
@@ -192,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
|
|||||||
}
|
}
|
||||||
return segment;
|
return segment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFolderIdFromServiceToken = async (
|
||||||
|
workspaceId: Types.ObjectId | string,
|
||||||
|
environment: string,
|
||||||
|
secretPath: string
|
||||||
|
) => {
|
||||||
|
const folders = await Folder.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folders) {
|
||||||
|
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
|
||||||
|
} else {
|
||||||
|
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||||
|
if (!folder) {
|
||||||
|
throw new Error("Folder not found");
|
||||||
|
}
|
||||||
|
return folder.id;
|
||||||
|
}
|
||||||
|
return "root";
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user