Address greptile suggestions

This commit is contained in:
Carlos Monastyrski
2025-08-13 11:23:55 -07:00
parent 7cf9d933da
commit b479406ba0
6 changed files with 589 additions and 527 deletions

View File

@@ -4,6 +4,7 @@ import axios from "axios";
import RE2 from "re2";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
@@ -27,34 +28,39 @@ type TCreateCouchbaseUser = {
}[];
};
type CouchbaseUserResponse = {
id: string;
uuid?: string;
};
const sanitizeCouchbaseUsername = (username: string): string => {
// Couchbase username restrictions:
// - Cannot contain: ) ( > < , ; : " \ / ] [ ? = } {
// - Cannot begin with @ character
const forbiddenCharsPattern = new RE2("[\\)\\(><,;:\"\\\\\\[\\]\\?=\\}\\{]", "g");
const forbiddenCharsPattern = new RE2('[\\)\\(><,;:"\\\\\\[\\]\\?=\\}\\{]', "g");
let sanitized = forbiddenCharsPattern.replace(username, "-");
const leadingAtPattern = new RE2("^@+");
sanitized = leadingAtPattern.replace(sanitized, "");
if (!sanitized || sanitized.length === 0) {
return alphaNumericNanoId(12);
}
return sanitized;
};
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(12);
if (!usernameTemplate) return sanitizeCouchbaseUsername(randomUsername);
const compiledUsername = compileUsernameTemplate({
usernameTemplate,
randomUsername,
identity
});
return sanitizeCouchbaseUsername(compiledUsername);
};
@@ -74,26 +80,26 @@ const generatePassword = (requirements?: PasswordRequirements): string => {
let remaining = length;
// Add required characters
for (let i = 0; i < required.lowercase; i++) {
for (let i = 0; i < required.lowercase; i += 1) {
password += lowercase[crypto.randomInt(lowercase.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.uppercase; i++) {
for (let i = 0; i < required.uppercase; i += 1) {
password += uppercase[crypto.randomInt(uppercase.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.digits; i++) {
for (let i = 0; i < required.digits; i += 1) {
password += digits[crypto.randomInt(digits.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.symbols; i++) {
for (let i = 0; i < required.symbols; i += 1) {
password += symbols[crypto.randomInt(symbols.length)];
remaining--;
remaining -= 1;
}
// Fill remaining with random characters from all sets
const allChars = lowercase + uppercase + digits + symbols;
for (let i = 0; i < remaining; i++) {
for (let i = 0; i < remaining; i += 1) {
password += allChars[crypto.randomInt(allChars.length)];
}
@@ -109,99 +115,92 @@ const couchbaseApiRequest = async (
url: string,
apiKey: string,
data?: unknown
): Promise<unknown> => {
): Promise<CouchbaseUserResponse> => {
await blockLocalAndPrivateIpAddresses(url);
try {
const response = await axios({
method: method.toLowerCase() as any,
method: method.toLowerCase() as "get" | "post" | "put" | "delete",
url,
headers: {
"Authorization": `Bearer ${apiKey}`,
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
data: data || undefined,
timeout: 30000
});
return response.data;
} catch (error: any) {
if (error.response) {
// Server responded with error status
const errorData = error.response.data;
const errorMessage = typeof errorData === 'string' ? errorData : JSON.stringify(errorData);
throw new BadRequestError({
message: `Couchbase API error: ${error.response.status} ${error.response.statusText} - ${errorMessage}`
});
} else if (error.request) {
// Request made but no response received
throw new BadRequestError({
message: `Couchbase API request failed: ${error.message}`
});
} else {
// Error in request configuration
throw new BadRequestError({
message: `Couchbase API request error: ${error.message}`
});
}
return response.data as CouchbaseUserResponse;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [apiKey]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
export const CouchbaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: object) => {
const providerInputs = DynamicSecretCouchbaseSchema.parse(inputs);
await blockLocalAndPrivateIpAddresses(providerInputs.url);
return providerInputs;
};
const validateConnection = async (inputs: unknown): Promise<boolean> => {
try {
const providerInputs = await validateProviderInputs(inputs as object);
// Test connection by trying to get organization info
const url = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}`;
await couchbaseApiRequest("GET", url, providerInputs.auth.apiKey);
return true;
} catch (error) {
throw new BadRequestError({
message: `Failed to connect to Couchbase: ${error instanceof Error ? error.message : "Unknown error"}`
throw new BadRequestError({
message: `Failed to connect to Couchbase: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
const create = async ({
inputs,
usernameTemplate,
identity
}: {
inputs: unknown;
usernameTemplate?: string | null;
identity?: { name: string };
const create = async ({
inputs,
usernameTemplate,
identity
}: {
inputs: unknown;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const providerInputs = await validateProviderInputs(inputs as object);
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(providerInputs.passwordRequirements);
const createUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users`;
let bucketResources;
if (typeof providerInputs.buckets === "string") {
// Simple string format - either "*" or comma-separated bucket names
const bucketNames = providerInputs.buckets === "*"
? ["*"]
: providerInputs.buckets.split(",").map(bucket => bucket.trim()).filter(bucket => bucket.length > 0);
bucketResources = bucketNames.map(bucketName => ({ name: bucketName }));
const bucketNames =
providerInputs.buckets === "*"
? ["*"]
: providerInputs.buckets
.split(",")
.map((bucket) => bucket.trim())
.filter((bucket) => bucket.length > 0);
bucketResources = bucketNames.map((bucketName) => ({ name: bucketName }));
} else {
// Array of bucket objects with scopes and collections
bucketResources = providerInputs.buckets.map(bucket => ({
bucketResources = providerInputs.buckets.map((bucket) => ({
name: bucket.name,
scopes: bucket.scopes?.map(scope => ({
scopes: bucket.scopes?.map((scope) => ({
name: scope.name,
collections: scope.collections || []
}))
@@ -211,18 +210,20 @@ export const CouchbaseProvider = (): TDynamicProviderFns => {
const userData: TCreateCouchbaseUser = {
name: username,
password,
access: [{
privileges: providerInputs.roles,
resources: {
buckets: bucketResources
access: [
{
privileges: providerInputs.roles,
resources: {
buckets: bucketResources
}
}
}]
]
};
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData) as any;
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData);
const userUuid = response?.id || response?.uuid || username;
return {
entityId: userUuid,
data: {
@@ -234,11 +235,11 @@ export const CouchbaseProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs as object);
const deleteUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users/${encodeURIComponent(entityId)}`;
await couchbaseApiRequest("DELETE", deleteUserUrl, providerInputs.auth.apiKey);
return { entityId };
};
@@ -255,4 +256,4 @@ export const CouchbaseProvider = (): TDynamicProviderFns => {
revoke,
renew
};
};
};

View File

@@ -511,16 +511,26 @@ export const DynamicSecretCouchbaseSchema = z.object({
projectId: z.string().trim().min(1).describe("Project ID"),
clusterId: z.string().trim().min(1).describe("Cluster ID"),
roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"),
buckets: z.union([
z.string().trim().min(1).default("*"),
z.array(z.object({
name: z.string().trim().min(1).describe("Bucket name"),
scopes: z.array(z.object({
name: z.string().trim().min(1).describe("Scope name"),
collections: z.array(z.string().trim().min(1)).optional().describe("Collection names")
})).optional().describe("Scopes within the bucket")
}))
]).default("*").describe("Bucket configuration: '*' for all buckets or array of bucket objects with scopes and collections"),
buckets: z
.union([
z.string().trim().min(1).default("*"),
z.array(
z.object({
name: z.string().trim().min(1).describe("Bucket name"),
scopes: z
.array(
z.object({
name: z.string().trim().min(1).describe("Scope name"),
collections: z.array(z.string().trim().min(1)).optional().describe("Collection names")
})
)
.optional()
.describe("Scopes within the bucket")
})
)
])
.default("*")
.describe("Bucket configuration: '*' for all buckets or array of bucket objects with scopes and collections"),
passwordRequirements: z
.object({
length: z.number().min(8, "Password must be at least 8 characters").max(128),
@@ -535,10 +545,13 @@ export const DynamicSecretCouchbaseSchema = z.object({
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '£'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);

View File

@@ -110,7 +110,7 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
{{randomUsername}} // infisical-3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
{{unixTimestamp}} // 17490641580
{{identity.name}} // testuser
{{random-5}} // x9k2m
{{random 5}} // x9k2m
{{truncate identity.name 4}} // test
{{replace identity.name 'user' 'replace'}} // testreplace
```
@@ -157,12 +157,12 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
![Provision Lease](../../../images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret.
@@ -170,7 +170,7 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
![Provision Lease](../../../images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
@@ -229,12 +229,12 @@ The advanced bucket configuration allows you to specify granular access control:
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you to see the expiration time of the lease or delete a lease before its set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
![Provision Lease](../../../images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
![Provision Lease](../../../images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret

View File

@@ -1,7 +1,8 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -27,8 +28,8 @@ import { WorkspaceEnv } from "@app/hooks/api/types";
import { slugSchema } from "@app/lib/schemas";
// Component for managing scopes and collections within a bucket
const BucketScopesConfiguration = ({
control,
const BucketScopesConfiguration = ({
control,
bucketIndex,
bucketsValue,
setValue,
@@ -36,8 +37,8 @@ const BucketScopesConfiguration = ({
removeScope,
addCollection,
removeCollection
}: {
control: any;
}: {
control: any;
bucketIndex: number;
bucketsValue: any;
setValue: any;
@@ -49,7 +50,6 @@ const BucketScopesConfiguration = ({
const bucket = Array.isArray(bucketsValue) ? bucketsValue[bucketIndex] : null;
const scopeFields = bucket?.scopes || [];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -66,11 +66,12 @@ const BucketScopesConfiguration = ({
</div>
{scopeFields.map((_scope: any, scopeIndex: number) => (
<div key={scopeIndex} className="p-3 bg-mineshaft-700 rounded border border-mineshaft-600 space-y-3">
<div
key={`scope-${scopeIndex + 1}`}
className="space-y-3 rounded border border-mineshaft-600 bg-mineshaft-700 p-3"
>
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-mineshaft-200">
Scope {scopeIndex + 1}
</h5>
<h5 className="text-xs font-medium text-mineshaft-200">Scope {scopeIndex + 1}</h5>
<IconButton
type="button"
variant="plain"
@@ -81,22 +82,18 @@ const BucketScopesConfiguration = ({
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`provider.buckets.${bucketIndex}.scopes.${scopeIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Scope Name"
isError={Boolean(error)}
errorText={error?.message}
>
<FormControl label="Scope Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., inventory, _default" className="text-sm" />
</FormControl>
)}
/>
<div className="pl-4 space-y-2">
<div className="space-y-2 pl-4">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-mineshaft-300">Collections</label>
<Button
@@ -109,48 +106,53 @@ const BucketScopesConfiguration = ({
Add Collection
</Button>
</div>
{scopeFields[scopeIndex]?.collections?.map((collection: string, collectionIndex: number) => (
<div key={`${bucketIndex}-${scopeIndex}-${collectionIndex}`} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[collectionIndex] = e.target.value;
setValue("provider.buckets", currentBuckets);
}
}}
placeholder="e.g., airport, airline"
className="text-sm"
/>
</FormControl>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove collection"
className="mb-4"
size="sm"
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
))}
{(!scopeFields[scopeIndex]?.collections || scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs text-mineshaft-400 italic">
{scopeFields[scopeIndex]?.collections?.map(
(collection: string, collectionIndex: number) => (
<div key={collection} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[
collectionIndex
] = e.target.value;
setValue("provider.buckets", currentBuckets);
}
}}
placeholder="e.g., airport, airline"
className="text-sm"
/>
</FormControl>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove collection"
className="mb-4"
size="sm"
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
)
)}
{(!scopeFields[scopeIndex]?.collections ||
scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs italic text-mineshaft-400">
No collections specified (access to all collections in scope)
</div>
)}
</div>
</div>
))}
{scopeFields.length === 0 && (
<div className="p-4 text-center border border-dashed border-mineshaft-600 rounded bg-mineshaft-700">
<p className="text-xs text-mineshaft-400 mb-2">
<div className="rounded border border-dashed border-mineshaft-600 bg-mineshaft-700 p-4 text-center">
<p className="mb-2 text-xs text-mineshaft-400">
No scopes configured (access to all scopes in bucket)
</p>
<Button
@@ -170,9 +172,21 @@ const BucketScopesConfiguration = ({
const couchbaseRoles = [
{ value: "data_reader", label: "Data Reader", description: "Read access to bucket data" },
{ value: "data_writer", label: "Data Writer", description: "Read and write access to bucket data" },
{ value: "read", label: "Read", description: "Read access to bucket data (alias for data_reader)" },
{ value: "write", label: "Write", description: "Read and write access to bucket data (alias for data_writer)" }
{
value: "data_writer",
label: "Data Writer",
description: "Read and write access to bucket data"
},
{
value: "read",
label: "Read",
description: "Read access to bucket data (alias for data_reader)"
},
{
value: "write",
label: "Write",
description: "Read and write access to bucket data (alias for data_writer)"
}
];
const passwordRequirementsSchema = z
@@ -189,10 +203,13 @@ const passwordRequirementsSchema = z
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '£'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
@@ -201,10 +218,14 @@ const passwordRequirementsSchema = z
const bucketSchema = z.object({
name: z.string().trim().min(1, "Bucket name is required"),
scopes: z.array(z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})).optional()
scopes: z
.array(
z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})
)
.optional()
});
const formSchema = z.object({
@@ -214,14 +235,11 @@ const formSchema = z.object({
projectId: z.string().trim().min(1),
clusterId: z.string().trim().min(1),
roles: z.array(z.string()).min(1, "At least one role must be selected"),
buckets: z.union([
z.string().trim().min(1),
z.array(bucketSchema)
]),
buckets: z.union([z.string().trim().min(1), z.array(bucketSchema)]),
useAdvancedBuckets: z.boolean().default(false),
passwordRequirements: passwordRequirementsSchema.optional(),
auth: z.object({
apiKey: z.string().trim()
apiKey: z.string().trim().min(1)
})
}),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -270,6 +288,7 @@ export const CouchbaseInputForm = ({
formState: { isSubmitting },
handleSubmit,
setValue,
getValues,
watch
} = useForm<TForm>({
resolver: zodResolver(formSchema),
@@ -329,7 +348,9 @@ export const CouchbaseInputForm = ({
const removeScope = (bucketIndex: number, scopeIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes) {
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter((_, i) => i !== scopeIndex);
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter(
(_, i) => i !== scopeIndex
);
setValue("provider.buckets", currentBuckets);
}
};
@@ -346,8 +367,9 @@ export const CouchbaseInputForm = ({
const removeCollection = (bucketIndex: number, scopeIndex: number, collectionIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections =
currentBuckets[bucketIndex].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
currentBuckets[bucketIndex].scopes[scopeIndex].collections = currentBuckets[
bucketIndex
].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
setValue("provider.buckets", currentBuckets);
}
};
@@ -362,14 +384,14 @@ export const CouchbaseInputForm = ({
}: TForm) => {
if (createDynamicSecret.isPending) return;
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
const transformedProvider = {
...provider,
buckets: provider.useAdvancedBuckets ? provider.buckets : (provider.buckets as string)
};
const { useAdvancedBuckets, ...finalProvider } = transformedProvider;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Couchbase, inputs: finalProvider },
@@ -462,38 +484,19 @@ export const CouchbaseInputForm = ({
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
placeholder="https://cloudapi.cloud.couchbase.com"
{...field}
/>
<Input placeholder="https://cloudapi.cloud.couchbase.com" {...field} />
</FormControl>
)}
/>
</div>
<div className="flex flex-col space-y-4">
<Controller
control={control}
name="provider.orgId"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization ID"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.projectId"
name="provider.orgId"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project ID"
label="Organization ID"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
@@ -502,207 +505,225 @@ export const CouchbaseInputForm = ({
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Controller
control={control}
name="provider.projectId"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project ID"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.clusterId"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Cluster ID"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="provider.clusterId"
defaultValue=""
render={({ field, fieldState: { error } }) => (
name="provider.roles"
defaultValue={["data_reader"]}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Cluster ID"
label="Roles"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="Select one or more roles to assign to the user"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="provider.roles"
defaultValue={["data_reader"]}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Roles"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="Select one or more roles to assign to the user"
>
<FilterableSelect
isMulti
value={couchbaseRoles.filter(role => value?.includes(role.value))}
onChange={(selectedRoles) => {
if (Array.isArray(selectedRoles)) {
onChange(selectedRoles.map((role: any) => role.value));
} else {
onChange([]);
}
}}
options={couchbaseRoles}
placeholder="Select roles..."
getOptionLabel={(option) => option.label}
getOptionValue={(option) => option.value}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.useAdvancedBuckets"
render={({ field: { value, onChange } }) => (
<FormControl
label="Advanced Bucket Configuration"
helperText="Enable to configure specific scopes and collections within buckets"
>
<Switch
id="advanced-buckets-switch"
isChecked={value}
onCheckedChange={(checked) => {
onChange(checked);
const bucketsController = control._formValues.provider.buckets;
if (checked && typeof bucketsController === "string") {
setValue("provider.buckets", []);
} else if (!checked && Array.isArray(bucketsController)) {
setValue("provider.buckets", "*");
}
}}
/>
</FormControl>
)}
/>
{!watch("provider.useAdvancedBuckets") && (
<Controller
control={control}
name="provider.buckets"
defaultValue="*"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Access"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets"
>
<Input
{...field}
value={typeof field.value === 'string' ? field.value : '*'}
onChange={(e) => field.onChange(e.target.value)}
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
<FilterableSelect
isMulti
value={couchbaseRoles.filter((role) => value?.includes(role.value))}
onChange={(selectedRoles) => {
if (Array.isArray(selectedRoles)) {
onChange(selectedRoles.map((role: any) => role.value));
} else {
onChange([]);
}
}}
options={couchbaseRoles}
placeholder="Select roles..."
getOptionLabel={(option) => option.label}
getOptionValue={(option) => option.value}
/>
</FormControl>
)}
/>
)}
{isAdvancedMode && Array.isArray(bucketsValue) && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium text-mineshaft-200">
Advanced Bucket Configuration
</div>
<div className="text-sm text-mineshaft-400">
Configure specific buckets with their scopes and collections
</div>
</div>
<Button
type="button"
variant="outline_bg"
size="sm"
onClick={addBucket}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Bucket
</Button>
</div>
<div className="space-y-4">
{Array.isArray(bucketsValue) && (bucketsValue as any[]).map((_, bucketIndex) => (
<div key={bucketIndex} className="p-4 border border-mineshaft-600 rounded bg-mineshaft-800 space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
</h4>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove bucket"
onClick={() => removeBucket(bucketIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`provider.buckets.${bucketIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="e.g., travel-sample" />
</FormControl>
)}
/>
<BucketScopesConfiguration
control={control}
bucketIndex={bucketIndex}
bucketsValue={bucketsValue}
setValue={setValue}
addScope={addScope}
removeScope={removeScope}
addCollection={addCollection}
removeCollection={removeCollection}
/>
</div>
))}
{(!Array.isArray(bucketsValue) || bucketsValue.length === 0) && (
<div className="p-8 text-center border border-dashed border-mineshaft-600 rounded">
<p className="text-sm text-mineshaft-400 mb-2">
No buckets configured
</p>
<Button
type="button"
variant="outline_bg"
size="sm"
onClick={addBucket}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add First Bucket
</Button>
</div>
)}
</div>
</div>
)}
<Controller
name="provider.auth.apiKey"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<Controller
control={control}
name="provider.useAdvancedBuckets"
render={({ field: { value, onChange } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className="w-full"
label="API Key"
label="Advanced Bucket Configuration"
helperText="Enable to configure specific scopes and collections within buckets"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
valueAlwaysHidden
rows={1}
wrap="hard"
onChange={(e) => onChange(e.target.value)}
/>
<Switch
id="advanced-buckets-switch"
isChecked={value}
onCheckedChange={(checked) => {
onChange(checked);
const bucketsController = getValues("provider.buckets");
if (checked && typeof bucketsController === "string") {
setValue("provider.buckets", []);
} else if (!checked && Array.isArray(bucketsController)) {
setValue("provider.buckets", "*");
}
}}
/>
</FormControl>
)}
/>
{!watch("provider.useAdvancedBuckets") && (
<Controller
control={control}
name="provider.buckets"
defaultValue="*"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Access"
className="w-full"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets"
>
<Input
{...field}
value={typeof field.value === "string" ? field.value : "*"}
onChange={(e) => field.onChange(e.target.value)}
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
/>
</FormControl>
)}
/>
)}
{isAdvancedMode && Array.isArray(bucketsValue) && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium text-mineshaft-200">
Advanced Bucket Configuration
</div>
<div className="text-sm text-mineshaft-400">
Configure specific buckets with their scopes and collections
</div>
</div>
<Button
type="button"
variant="outline_bg"
size="sm"
onClick={addBucket}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Bucket
</Button>
</div>
<div className="space-y-4">
{Array.isArray(bucketsValue) &&
(bucketsValue as any[]).map((_, bucketIndex) => (
<div
key={`bucket-${bucketIndex + 1}`}
className="space-y-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
</h4>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove bucket"
onClick={() => removeBucket(bucketIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`provider.buckets.${bucketIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="e.g., travel-sample" />
</FormControl>
)}
/>
<BucketScopesConfiguration
control={control}
bucketIndex={bucketIndex}
bucketsValue={bucketsValue}
setValue={setValue}
addScope={addScope}
removeScope={removeScope}
addCollection={addCollection}
removeCollection={removeCollection}
/>
</div>
))}
{(!Array.isArray(bucketsValue) || bucketsValue.length === 0) && (
<div className="rounded border border-dashed border-mineshaft-600 p-8 text-center">
<p className="mb-2 text-sm text-mineshaft-400">No buckets configured</p>
<Button
type="button"
variant="outline_bg"
size="sm"
onClick={addBucket}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add First Bucket
</Button>
</div>
)}
</div>
</div>
)}
<Controller
name="provider.auth.apiKey"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className="w-full"
label="API Key"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
valueAlwaysHidden
rows={1}
wrap="hard"
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
@@ -731,7 +752,7 @@ export const CouchbaseInputForm = ({
<div className="flex items-center space-x-2">
<span>Password Configuration (optional)</span>
<Tooltip content="Couchbase password requirements: minimum 8 characters, at least 1 uppercase, 1 lowercase, 1 digit, 1 special character. Cannot contain: < > ; . * & | £">
<div className="h-4 w-4 rounded-full bg-mineshaft-600 text-xs text-mineshaft-300 flex items-center justify-center cursor-help">
<div className="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-mineshaft-600 text-xs text-mineshaft-300">
?
</div>
</Tooltip>
@@ -750,7 +771,7 @@ export const CouchbaseInputForm = ({
<Controller
control={control}
name="provider.passwordRequirements.length"
defaultValue={48}
defaultValue={12}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"

View File

@@ -30,6 +30,7 @@ import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm";
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
import { CassandraInputForm } from "./CassandraInputForm";
import { CouchbaseInputForm } from "./CouchbaseInputForm";
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
import { GcpIamInputForm } from "./GcpIamInputForm";
import { GithubInputForm } from "./GithubInputForm";
@@ -45,7 +46,6 @@ import { SnowflakeInputForm } from "./SnowflakeInputForm";
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
import { TotpInputForm } from "./TotpInputForm";
import { VerticaInputForm } from "./VerticaInputForm";
import { CouchbaseInputForm } from "./CouchbaseInputForm";
type Props = {
isOpen?: boolean;
@@ -615,7 +615,7 @@ export const CreateDynamicSecretForm = ({
/>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === DynamicSecretProviders.Couchbase && (
<motion.div
key="dynamic-couchbase-step"

View File

@@ -1,7 +1,8 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -27,8 +28,8 @@ import { slugSchema } from "@app/lib/schemas";
import { MetadataForm } from "../MetadataForm";
const BucketScopesConfiguration = ({
control,
const BucketScopesConfiguration = ({
control,
bucketIndex,
bucketsValue,
setValue,
@@ -36,8 +37,8 @@ const BucketScopesConfiguration = ({
removeScope,
addCollection,
removeCollection
}: {
control: any;
}: {
control: any;
bucketIndex: number;
bucketsValue: any;
setValue: any;
@@ -49,7 +50,6 @@ const BucketScopesConfiguration = ({
const bucket = Array.isArray(bucketsValue) ? bucketsValue[bucketIndex] : null;
const scopeFields = bucket?.scopes || [];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -66,11 +66,12 @@ const BucketScopesConfiguration = ({
</div>
{scopeFields.map((_scope: any, scopeIndex: number) => (
<div key={scopeIndex} className="p-3 bg-mineshaft-700 rounded border border-mineshaft-600 space-y-3">
<div
key={`scope-${scopeIndex + 1}`}
className="space-y-3 rounded border border-mineshaft-600 bg-mineshaft-700 p-3"
>
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-mineshaft-200">
Scope {scopeIndex + 1}
</h5>
<h5 className="text-xs font-medium text-mineshaft-200">Scope {scopeIndex + 1}</h5>
<IconButton
type="button"
variant="plain"
@@ -81,22 +82,18 @@ const BucketScopesConfiguration = ({
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`inputs.buckets.${bucketIndex}.scopes.${scopeIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Scope Name"
isError={Boolean(error)}
errorText={error?.message}
>
<FormControl label="Scope Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., inventory, _default" className="text-sm" />
</FormControl>
)}
/>
<div className="pl-4 space-y-2">
<div className="space-y-2 pl-4">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-mineshaft-300">Collections</label>
<Button
@@ -109,47 +106,52 @@ const BucketScopesConfiguration = ({
Add Collection
</Button>
</div>
{scopeFields[scopeIndex]?.collections?.map((collection: string, collectionIndex: number) => (
<div key={`${bucketIndex}-${scopeIndex}-${collectionIndex}`} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[collectionIndex] = e.target.value;
setValue("inputs.buckets", currentBuckets);
}
}}
placeholder="e.g., airport, airline"
className="text-sm"
/>
</FormControl>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove collection"
size="sm"
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
))}
{(!scopeFields[scopeIndex]?.collections || scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs text-mineshaft-400 italic">
{scopeFields[scopeIndex]?.collections?.map(
(collection: string, collectionIndex: number) => (
<div key={collection} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[
collectionIndex
] = e.target.value;
setValue("inputs.buckets", currentBuckets);
}
}}
placeholder="e.g., airport, airline"
className="text-sm"
/>
</FormControl>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove collection"
size="sm"
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
)
)}
{(!scopeFields[scopeIndex]?.collections ||
scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs italic text-mineshaft-400">
No collections specified (access to all collections in scope)
</div>
)}
</div>
</div>
))}
{scopeFields.length === 0 && (
<div className="p-4 text-center border border-dashed border-mineshaft-600 rounded bg-mineshaft-700">
<p className="text-xs text-mineshaft-400 mb-2">
<div className="rounded border border-dashed border-mineshaft-600 bg-mineshaft-700 p-4 text-center">
<p className="mb-2 text-xs text-mineshaft-400">
No scopes configured (access to all scopes in bucket)
</p>
<Button
@@ -169,9 +171,21 @@ const BucketScopesConfiguration = ({
const couchbaseRoles = [
{ value: "data_reader", label: "Data Reader", description: "Read access to bucket data" },
{ value: "data_writer", label: "Data Writer", description: "Read and write access to bucket data" },
{ value: "read", label: "Read", description: "Read access to bucket data (alias for data_reader)" },
{ value: "write", label: "Write", description: "Read and write access to bucket data (alias for data_writer)" }
{
value: "data_writer",
label: "Data Writer",
description: "Read and write access to bucket data"
},
{
value: "read",
label: "Read",
description: "Read access to bucket data (alias for data_reader)"
},
{
value: "write",
label: "Write",
description: "Read and write access to bucket data (alias for data_writer)"
}
];
const passwordRequirementsSchema = z
@@ -188,10 +202,13 @@ const passwordRequirementsSchema = z
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '<27>'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | <20>").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "<22>"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * &")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
@@ -200,10 +217,14 @@ const passwordRequirementsSchema = z
const bucketSchema = z.object({
name: z.string().trim().min(1, "Bucket name is required"),
scopes: z.array(z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})).optional()
scopes: z
.array(
z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})
)
.optional()
});
const formSchema = z.object({
@@ -214,10 +235,7 @@ const formSchema = z.object({
projectId: z.string().min(1),
clusterId: z.string().min(1),
roles: z.array(z.string()).min(1),
buckets: z.union([
z.string().trim().min(1),
z.array(bucketSchema)
]),
buckets: z.union([z.string().trim().min(1), z.array(bucketSchema)]),
useAdvancedBuckets: z.boolean().default(false),
passwordRequirements: passwordRequirementsSchema.optional(),
auth: z.object({
@@ -287,7 +305,7 @@ export const EditDynamicSecretCouchbaseForm = ({
metadata: dynamicSecret.metadata,
usernameTemplate: dynamicSecret.usernameTemplate,
inputs: {
...dynamicSecret.inputs as any,
...(dynamicSecret.inputs as any),
useAdvancedBuckets: Array.isArray((dynamicSecret.inputs as any)?.buckets)
}
}
@@ -324,7 +342,9 @@ export const EditDynamicSecretCouchbaseForm = ({
const removeScope = (bucketIndex: number, scopeIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes) {
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter((_, i) => i !== scopeIndex);
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter(
(_, i) => i !== scopeIndex
);
setValue("inputs.buckets", currentBuckets);
}
};
@@ -341,31 +361,36 @@ export const EditDynamicSecretCouchbaseForm = ({
const removeCollection = (bucketIndex: number, scopeIndex: number, collectionIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections =
currentBuckets[bucketIndex].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
currentBuckets[bucketIndex].scopes[scopeIndex].collections = currentBuckets[
bucketIndex
].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
setValue("inputs.buckets", currentBuckets);
}
};
const handleUpdateDynamicSecret = async ({
inputs,
newName,
defaultTTL,
maxTTL,
metadata,
usernameTemplate
const handleUpdateDynamicSecret = async ({
inputs,
newName,
defaultTTL,
maxTTL,
metadata,
usernameTemplate
}: TForm) => {
if (updateDynamicSecret.isPending) return;
const transformedInputs = inputs ? {
...inputs,
buckets: inputs.useAdvancedBuckets ? inputs.buckets : (inputs.buckets as string)
} : inputs;
const finalInputs = transformedInputs ? (() => {
const { useAdvancedBuckets, ...rest } = transformedInputs;
return rest;
})() : transformedInputs;
const transformedInputs = inputs
? {
...inputs,
buckets: inputs.useAdvancedBuckets ? inputs.buckets : (inputs.buckets as string)
}
: inputs;
const finalInputs = transformedInputs
? (() => {
const { useAdvancedBuckets, ...rest } = transformedInputs;
return rest;
})()
: transformedInputs;
try {
await updateDynamicSecret.mutateAsync({
@@ -378,7 +403,10 @@ export const EditDynamicSecretCouchbaseForm = ({
maxTTL: maxTTL || undefined,
newName: newName === dynamicSecret.name ? undefined : newName,
metadata,
usernameTemplate: !usernameTemplate || usernameTemplate === "{{randomUsername}}" ? undefined : usernameTemplate,
usernameTemplate:
!usernameTemplate || usernameTemplate === "{{randomUsername}}"
? undefined
: usernameTemplate,
inputs: finalInputs
}
});
@@ -392,7 +420,7 @@ export const EditDynamicSecretCouchbaseForm = ({
} catch (err) {
createNotification({
type: "error",
text: "Failed to update dynamic secret"
text: `Failed to update dynamic secret: ${err}`
});
}
};
@@ -468,10 +496,7 @@ export const EditDynamicSecretCouchbaseForm = ({
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
placeholder="https://cloudapi.cloud.couchbase.com"
{...field}
/>
<Input placeholder="https://cloudapi.cloud.couchbase.com" {...field} />
</FormControl>
)}
/>
@@ -534,7 +559,7 @@ export const EditDynamicSecretCouchbaseForm = ({
>
<FilterableSelect
isMulti
value={couchbaseRoles.filter(role => value?.includes(role.value))}
value={couchbaseRoles.filter((role) => value?.includes(role.value))}
onChange={(selectedRoles) => {
if (Array.isArray(selectedRoles)) {
onChange(selectedRoles.map((role: any) => role.value));
@@ -558,7 +583,7 @@ export const EditDynamicSecretCouchbaseForm = ({
label="Advanced Bucket Configuration"
helperText="Enable to configure specific scopes and collections within buckets"
>
<Switch
<Switch
id="advanced-buckets-switch"
isChecked={value}
onCheckedChange={(checked) => {
@@ -588,9 +613,9 @@ export const EditDynamicSecretCouchbaseForm = ({
errorText={error?.message}
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets"
>
<Input
{...field}
value={typeof field.value === 'string' ? field.value : '*'}
<Input
{...field}
value={typeof field.value === "string" ? field.value : "*"}
onChange={(e) => field.onChange(e.target.value)}
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
/>
@@ -622,54 +647,56 @@ export const EditDynamicSecretCouchbaseForm = ({
</div>
<div className="space-y-4">
{Array.isArray(bucketsValue) && (bucketsValue as any[]).map((_, bucketIndex) => (
<div key={bucketIndex} className="p-4 border border-mineshaft-600 rounded bg-mineshaft-800 space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
</h4>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove bucket"
onClick={() => removeBucket(bucketIndex)}
>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`inputs.buckets.${bucketIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Name"
isError={Boolean(error)}
errorText={error?.message}
{Array.isArray(bucketsValue) &&
(bucketsValue as any[]).map((_, bucketIndex) => (
<div
key={`bucket-${bucketIndex + 1}`}
className="space-y-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
</h4>
<IconButton
type="button"
variant="plain"
ariaLabel="Remove bucket"
onClick={() => removeBucket(bucketIndex)}
>
<Input {...field} placeholder="e.g., travel-sample" />
</FormControl>
)}
/>
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
<Controller
control={control}
name={`inputs.buckets.${bucketIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bucket Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="e.g., travel-sample" />
</FormControl>
)}
/>
<BucketScopesConfiguration
control={control}
bucketIndex={bucketIndex}
bucketsValue={bucketsValue}
setValue={setValue}
addScope={addScope}
removeScope={removeScope}
addCollection={addCollection}
removeCollection={removeCollection}
/>
</div>
))}
<BucketScopesConfiguration
control={control}
bucketIndex={bucketIndex}
bucketsValue={bucketsValue}
setValue={setValue}
addScope={addScope}
removeScope={removeScope}
addCollection={addCollection}
removeCollection={removeCollection}
/>
</div>
))}
{(!Array.isArray(bucketsValue) || bucketsValue.length === 0) && (
<div className="p-8 text-center border border-dashed border-mineshaft-600 rounded">
<p className="text-sm text-mineshaft-400 mb-2">
No buckets configured
</p>
<div className="rounded border border-dashed border-mineshaft-600 p-8 text-center">
<p className="mb-2 text-sm text-mineshaft-400">No buckets configured</p>
<Button
type="button"
variant="outline_bg"
@@ -729,7 +756,7 @@ export const EditDynamicSecretCouchbaseForm = ({
<div className="flex items-center space-x-2">
<span>Password Configuration (optional)</span>
<Tooltip content="Couchbase password requirements: minimum 8 characters, at least 1 uppercase, 1 lowercase, 1 digit, 1 special character. Cannot contain: < > ; . * & | <20>">
<div className="h-4 w-4 rounded-full bg-mineshaft-600 text-xs text-mineshaft-300 flex items-center justify-center cursor-help">
<div className="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-mineshaft-600 text-xs text-mineshaft-300">
?
</div>
</Tooltip>
@@ -904,4 +931,4 @@ export const EditDynamicSecretCouchbaseForm = ({
</form>
</div>
);
};
};