mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +00:00
Merge remote-tracking branch 'origin' into scim
This commit is contained in:
@@ -28,7 +28,7 @@ jobs:
|
|||||||
run: docker build --tag infisical-api .
|
run: docker build --tag infisical-api .
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker-compose -f docker-compose.prod.yml up -d db redis
|
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start the server
|
- name: Start the server
|
||||||
run: |
|
run: |
|
||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
@@ -70,6 +70,6 @@ jobs:
|
|||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.pg.yml" down
|
docker-compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api
|
||||||
docker remove infisical-api
|
docker remove infisical-api
|
||||||
|
|||||||
Generated
+401
-1065
File diff suppressed because it is too large
Load Diff
@@ -81,7 +81,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"aws-sdk": "^2.1532.0",
|
"aws-sdk": "^2.1545.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.4",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.1.1",
|
"bullmq": "^5.1.1",
|
||||||
@@ -109,7 +109,8 @@
|
|||||||
"mysql2": "^3.6.5",
|
"mysql2": "^3.6.5",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.9",
|
||||||
|
"ora": "^7.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.0",
|
"posthog-node": "^3.6.0",
|
||||||
"probot": "^12.3.3",
|
"probot": "^13.0.0",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({
|
const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({
|
||||||
...req.body,
|
...req.body,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent
|
userAgent,
|
||||||
|
authorization: req.headers.authorization as string
|
||||||
});
|
});
|
||||||
|
|
||||||
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
|
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
|
||||||
|
|||||||
@@ -212,13 +212,16 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag
|
encryptedPrivateKeyTag,
|
||||||
|
authorization
|
||||||
}: TCompleteAccountInviteDTO) => {
|
}: TCompleteAccountInviteDTO) => {
|
||||||
const user = await userDAL.findUserByEmail(email);
|
const user = await userDAL.findUserByEmail(email);
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
throw new Error("Failed to complete account for complete user");
|
throw new Error("Failed to complete account for complete user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSignUpAuthorization(authorization, user.id);
|
||||||
|
|
||||||
const [orgMembership] = await orgDAL.findMembership({
|
const [orgMembership] = await orgDAL.findMembership({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: OrgMembershipStatus.Invited
|
status: OrgMembershipStatus.Invited
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ export type TCompleteAccountInviteDTO = {
|
|||||||
verifier: string;
|
verifier: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
authorization: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({
|
|||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
environmentInfo: importEnv,
|
environmentInfo: importEnv,
|
||||||
folderId: importedFolders?.[i]?.id,
|
folderId: importedFolders?.[i]?.id,
|
||||||
secrets: importedFolders?.[i]?.id
|
// this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path
|
||||||
? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({
|
secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
}))
|
}))
|
||||||
: []
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ description: "How to use Infisical for secret management in Ansible"
|
|||||||
|
|
||||||
The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
|
The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
|
||||||
|
|
||||||
<Info>
|
## Troubleshoot
|
||||||
Have any questions? Join Infisical's [community Slack](https://infisical.com/slack) for quick support.
|
|
||||||
</Info>
|
<Accordion title="I'm getting a error related to objc[72832]: +[__NSCFConstantString initialize]">
|
||||||
|
If you get this Python error when you running the lookup plugin:-
|
||||||
|
|
||||||
|
```
|
||||||
|
objc[72832]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
|
||||||
|
Fatal Python error: Aborted
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to add this to your shell environment or ansible wrapper script:-
|
||||||
|
|
||||||
|
```
|
||||||
|
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ CMD ["infisical", "run", "--", "npm", "run", "start"]
|
|||||||
CMD ["infisical", "run", "--command", "npm run start && ..."]
|
CMD ["infisical", "run", "--command", "npm run start && ..."]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate an service token
|
## Generate a service token
|
||||||
|
|
||||||
Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token).
|
Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token).
|
||||||
This service token will allow you to authenticate and fetch secrets from Infisical.
|
This service token will allow you to authenticate and fetch secrets from Infisical.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ Other environment variables are listed below to increase the functionality of yo
|
|||||||
Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32`
|
Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32`
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField query="MONGO_URL" type="string" default="none" required>
|
<ParamField query="DB_CONNECTION_URI" type="string" default="none" required>
|
||||||
Mongo connection string. *TLS based connection string is not yet supported
|
Postgres database connection string.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField query="REDIS_URL" type="string" default="none" required>
|
<ParamField query="REDIS_URL" type="string" default="none" required>
|
||||||
|
|||||||
@@ -2,53 +2,79 @@
|
|||||||
title: "Docker Compose"
|
title: "Docker Compose"
|
||||||
description: "Run Infisical with Docker Compose template"
|
description: "Run Infisical with Docker Compose template"
|
||||||
---
|
---
|
||||||
|
Install Infisical using Docker compose. This self hosting method contains all of the required components needed
|
||||||
|
to run a functional instance of Infisical.
|
||||||
|
|
||||||
<Steps>
|
## Prerequisites
|
||||||
<Step title="Install Docker on your VM">
|
- [Docker](https://docs.docker.com/engine/install/)
|
||||||
```bash
|
- [Docker compose](https://docs.docker.com/compose/install/)
|
||||||
# Example in ubuntu
|
|
||||||
apt-get update
|
|
||||||
apt-get upgrade
|
|
||||||
apt install docker-compose
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Download required files">
|
|
||||||
2.1. Run the command below to download the `.env` file template.
|
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
This Docker Compose configuration is not designed for high-availability production scenarios.
|
||||||
|
It includes just the essential components needed to set up an Infisical proof of concept (POC).
|
||||||
|
Additional configuration is required to enhance data redundancy and ensure higher availability for production environments.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Verify prerequisites
|
||||||
|
To verify that Docker compose and Docker are installed on the machine where you plan to install Infisical, run the following commands.
|
||||||
|
|
||||||
|
Check for docker installation
|
||||||
```bash
|
```bash
|
||||||
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
|
docker
|
||||||
```
|
```
|
||||||
|
|
||||||
2.2. Run the command below to download the docker compose template.
|
Check for docker compose installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml
|
docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
2.3. Run the command below to download the `nginx` config file.
|
## Download docker compose file
|
||||||
|
You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`.
|
||||||
|
If your system doesn't have either of these, you can use a equivalent command that works with your machine.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="curl">
|
||||||
```bash
|
```bash
|
||||||
mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
|
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
</Step>
|
<Tab title="wget">
|
||||||
<Step title="Update the .env file">
|
|
||||||
Running Infisical requires a few environment variables to be set.
|
|
||||||
At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL` which you can read more about [here](/self-hosting/configuration/envars).
|
|
||||||
|
|
||||||
Tweak the `.env` accordingly.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nano .env
|
wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
|
||||||
```
|
```
|
||||||
</Step>
|
</Tab>
|
||||||
<Step title="Start Infisical">
|
</Tabs>
|
||||||
Finally, run the command below to get Infisical up and running (in detached mode).
|
|
||||||
|
|
||||||
|
## Configure instance credentials
|
||||||
|
Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc.
|
||||||
|
The default credentials can be downloaded using the one of the commands listed below.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="curl">
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.yml up -d
|
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="wget">
|
||||||
|
```bash
|
||||||
|
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
Your Infisical installation is complete and should be running on port `80` or `http://localhost:80`.
|
Once downloaded, the credentials file will be saved to your working directly as `.env` file.
|
||||||
</Step>
|
View all available configurations [here](/self-hosting/configuration/envars).
|
||||||
</Steps>
|
|
||||||
|
<Warning>
|
||||||
|
The default .env file contains credentials that are intended solely for testing purposes.
|
||||||
|
For production use, please generate a new `ENCRYPTION_KEY` and `AUTH_SECRET`. Instructions to do so, can be found [here](/self-hosting/configuration/envars)
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Start Infisical
|
||||||
|
Run the command below to start Infisical and all related services.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`.
|
||||||
Generated
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "npm-proj-1708142396879-0.7491636790514078NCvb8i",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
"yaml": "^2.2.2",
|
"yaml": "^2.2.2",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zod": "^3.22.3",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^4.4.1"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^7.5.2",
|
"@storybook/addon-essentials": "^7.5.2",
|
||||||
@@ -23536,9 +23536,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "4.4.7",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz",
|
||||||
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
|
"integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"use-sync-external-store": "1.2.0"
|
"use-sync-external-store": "1.2.0"
|
||||||
},
|
},
|
||||||
@@ -23547,7 +23547,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": ">=16.8",
|
"@types/react": ">=16.8",
|
||||||
"immer": ">=9.0",
|
"immer": ">=9.0.6",
|
||||||
"react": ">=16.8"
|
"react": ">=16.8"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
"yaml": "^2.2.2",
|
"yaml": "^2.2.2",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zod": "^3.22.3",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^4.4.1"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^7.5.2",
|
"@storybook/addon-essentials": "^7.5.2",
|
||||||
|
|||||||
+38
-19
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faClock } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -9,20 +9,28 @@ import { z } from "zod";
|
|||||||
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
|
||||||
|
|
||||||
const ReminderFormSchema = z.object({
|
const ReminderFormSchema = z.object({
|
||||||
note: z.string().optional(),
|
note: z.string().optional().nullable(),
|
||||||
days: z
|
days: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, { message: "Must be at least 1 day" })
|
.min(1, { message: "Must be at least 1 day" })
|
||||||
.max(365, { message: "Must be less than 365 days" })
|
.max(365, { message: "Must be less than 365 days" })
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||||
|
|
||||||
interface ReminderFormProps {
|
interface ReminderFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
repeatDays?: number | null;
|
||||||
|
note?: string | null;
|
||||||
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps) => {
|
export const CreateReminderForm = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
repeatDays,
|
||||||
|
note
|
||||||
|
}: ReminderFormProps) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
@@ -31,32 +39,31 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TReminderFormSchema>({
|
} = useForm<TReminderFormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
days: repeatDays || undefined,
|
||||||
|
note: note || ""
|
||||||
|
},
|
||||||
resolver: zodResolver(ReminderFormSchema)
|
resolver: zodResolver(ReminderFormSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||||
console.log(data);
|
|
||||||
onOpenChange(false, data);
|
onOpenChange(false, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
reset();
|
reset({
|
||||||
|
days: repeatDays || undefined,
|
||||||
|
note: note || ""
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create secret reminder"
|
title={`${repeatDays ? "Update" : "Create"} reminder`}
|
||||||
// ? QUESTION: Should this specifically say its for secret rotation?
|
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
||||||
// ? Or should we be call it something more generic?
|
|
||||||
subTitle={
|
|
||||||
<div>
|
|
||||||
Set up a reminder for when this secret should be rotated. Everyone with access to this
|
|
||||||
project will be notified when the reminder is triggered.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -68,7 +75,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
<>
|
<>
|
||||||
<FormControl
|
<FormControl
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
label="How many days between"
|
label="Reminder Interval (in days)"
|
||||||
isError={Boolean(fieldState.error)}
|
isError={Boolean(fieldState.error)}
|
||||||
errorText={fieldState.error?.message || ""}
|
errorText={fieldState.error?.message || ""}
|
||||||
>
|
>
|
||||||
@@ -76,6 +83,7 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="31"
|
placeholder="31"
|
||||||
|
value={field.value || undefined}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div
|
<div
|
||||||
@@ -84,7 +92,8 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
field.value ? "opacity-60" : "opacity-0"
|
field.value ? "opacity-60" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Every {field.value > 1 ? `${field.value} days` : "day"}
|
A reminder will be sent every{" "}
|
||||||
|
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -102,17 +111,27 @@ export const CreateReminderForm = ({ isOpen, onOpenChange }: ReminderFormProps)
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-7 flex items-center">
|
<div className="mt-7 flex items-center space-x-4">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
key="layout-create-project-submit"
|
||||||
className="mr-4"
|
className=""
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create reminder
|
{repeatDays ? "Update" : "Create"} reminder
|
||||||
</Button>
|
</Button>
|
||||||
|
{repeatDays && (
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={() => onOpenChange(false, { days: null, note: null })}
|
||||||
|
colorSchema="danger"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete reminder
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
key="layout-cancel-create-project"
|
key="layout-cancel-create-project"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
|
|||||||
+297
-239
@@ -5,6 +5,7 @@ import {
|
|||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleDot,
|
faCircleDot,
|
||||||
|
faClock,
|
||||||
faPlus,
|
faPlus,
|
||||||
faTag
|
faTag
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -33,9 +34,11 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { CreateReminderForm } from "./CreateReminderForm";
|
||||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -147,262 +150,317 @@ export const SecretDetailSidebar = ({
|
|||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
|
|
||||||
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<>
|
||||||
onOpenChange={(state) => {
|
<CreateReminderForm
|
||||||
if (isOpen && isDirty) {
|
repeatDays={secretReminderRepeatDays}
|
||||||
if (
|
note={secretReminderNote}
|
||||||
// eslint-disable-next-line no-alert
|
isOpen={createReminderFormOpen}
|
||||||
window.confirm("You have edited the secret. Are you sure you want to reset the change?")
|
onOpenChange={(_, data) => {
|
||||||
) {
|
setCreateReminderFormOpen.toggle();
|
||||||
onToggle(false);
|
|
||||||
reset();
|
if (data) {
|
||||||
} else return;
|
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
|
||||||
}
|
setValue("reminderNote", data.note, { shouldDirty: true });
|
||||||
onToggle(state);
|
}
|
||||||
}}
|
}}
|
||||||
isOpen={isOpen}
|
/>
|
||||||
>
|
<Drawer
|
||||||
<DrawerContent title="Secret">
|
onOpenChange={(state) => {
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
if (isOpen && isDirty) {
|
||||||
<div className="flex h-full flex-col">
|
if (
|
||||||
<FormControl label="Key">
|
// eslint-disable-next-line no-alert
|
||||||
<Input isDisabled {...register("key")} />
|
window.confirm(
|
||||||
</FormControl>
|
"You have edited the secret. Are you sure you want to reset the change?"
|
||||||
<ProjectPermissionCan
|
)
|
||||||
I={ProjectPermissionActions.Edit}
|
) {
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
onToggle(false);
|
||||||
>
|
reset();
|
||||||
{(isAllowed) => (
|
} else return;
|
||||||
<Controller
|
}
|
||||||
name="value"
|
onToggle(state);
|
||||||
key="secret-value"
|
}}
|
||||||
control={control}
|
isOpen={isOpen}
|
||||||
render={({ field }) => (
|
>
|
||||||
<FormControl label="Value">
|
<DrawerContent title="Secret">
|
||||||
<SecretInput
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||||
isReadOnly={isReadOnly}
|
<div className="flex h-full flex-col">
|
||||||
key="secret-value"
|
<FormControl label="Key">
|
||||||
isDisabled={isOverridden || !isAllowed}
|
<Input isDisabled {...register("key")} />
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
</FormControl>
|
||||||
{...field}
|
|
||||||
autoFocus={false}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Controller
|
||||||
isDisabled={!isAllowed}
|
name="value"
|
||||||
id="personal-override"
|
key="secret-value"
|
||||||
onCheckedChange={handleOverrideClick}
|
control={control}
|
||||||
isChecked={isOverridden}
|
render={({ field }) => (
|
||||||
>
|
<FormControl label="Value">
|
||||||
Override with a personal value
|
<SecretInput
|
||||||
</Switch>
|
isReadOnly={isReadOnly}
|
||||||
|
key="secret-value"
|
||||||
|
isDisabled={isOverridden || !isAllowed}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||||
|
{...field}
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
||||||
{isOverridden && (
|
|
||||||
<Controller
|
|
||||||
name="valueOverride"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl label="Value Override">
|
|
||||||
<SecretInput
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl label="Tags" className="">
|
|
||||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
|
||||||
{fields.map(({ tagColor, id: formId, name, id }) => (
|
|
||||||
<Tag
|
|
||||||
className="flex w-min items-center space-x-2"
|
|
||||||
key={formId}
|
|
||||||
onClose={() => {
|
|
||||||
if (cannotEditSecret) {
|
|
||||||
createNotification({ type: "error", text: "Access denied" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
|
||||||
if (tag) handleTagSelect(tag);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-3 w-3 rounded-full"
|
|
||||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">{name}</div>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
<DropdownMenu>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="add"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
className="rounded-md"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<DropdownMenuContent align="end" className="z-[100]">
|
|
||||||
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
const { id: tagId, name, color } = tag;
|
|
||||||
|
|
||||||
const isSelected = selectedTagsGroupById?.[tagId];
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleTagSelect(tag)}
|
|
||||||
key={tagId}
|
|
||||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
iconPos="right"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="mr-2 h-2 w-2 rounded-full"
|
|
||||||
style={{ background: color || "#bec2c8" }}
|
|
||||||
/>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Create}
|
|
||||||
a={ProjectPermissionSub.Tags}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
className="w-full"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
|
||||||
onClick={onCreateTag}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Create a tag
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl label="Comments & Notes">
|
|
||||||
<TextArea
|
|
||||||
className="border border-mineshaft-600 text-sm"
|
|
||||||
{...register("comment")}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
rows={5}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="skipMultilineEncoding"
|
|
||||||
render={({ field: { value, onChange, onBlur } }) => (
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Switch
|
|
||||||
id="skipmultiencoding-option"
|
|
||||||
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
|
||||||
isChecked={!value}
|
|
||||||
onBlur={onBlur}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
className="items-center"
|
|
||||||
>
|
|
||||||
Enable multi line encoding
|
|
||||||
<Tooltip
|
|
||||||
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
|
|
||||||
className="z-[100]"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
</Switch>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
|
||||||
<div className="mb-2">Version History</div>
|
|
||||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
|
||||||
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
|
||||||
<div key={id} className="flex flex-col space-y-1">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
|
||||||
</div>
|
|
||||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
|
||||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
|
||||||
<div className="break-all font-mono">{value}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Switch
|
||||||
isFullWidth
|
isDisabled={!isAllowed}
|
||||||
type="submit"
|
id="personal-override"
|
||||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
onCheckedChange={handleOverrideClick}
|
||||||
isLoading={isSubmitting}
|
isChecked={isOverridden}
|
||||||
>
|
>
|
||||||
Save Changes
|
Override with a personal value
|
||||||
</Button>
|
</Switch>
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
{isOverridden && (
|
||||||
|
<Controller
|
||||||
|
name="valueOverride"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl label="Value Override">
|
||||||
|
<SecretInput
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FormControl label="Tags" className="">
|
||||||
|
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||||
|
{fields.map(({ tagColor, id: formId, name, id }) => (
|
||||||
|
<Tag
|
||||||
|
className="flex w-min items-center space-x-2"
|
||||||
|
key={formId}
|
||||||
|
onClose={() => {
|
||||||
|
if (cannotEditSecret) {
|
||||||
|
createNotification({ type: "error", text: "Access denied" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||||
|
if (tag) handleTagSelect(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-3 w-3 rounded-full"
|
||||||
|
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{name}</div>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
<DropdownMenu>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="add"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="rounded-md"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<DropdownMenuContent align="end" className="z-[100]">
|
||||||
|
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const { id: tagId, name, color } = tag;
|
||||||
|
|
||||||
|
const isSelected = selectedTagsGroupById?.[tagId];
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleTagSelect(tag)}
|
||||||
|
key={tagId}
|
||||||
|
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="mr-2 h-2 w-2 rounded-full"
|
||||||
|
style={{ background: color || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Tags}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
className="w-full"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||||
|
onClick={onCreateTag}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create a tag
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Reminder">
|
||||||
|
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||||
|
<div className="mt-2 ml-1 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||||
|
<span className="text-sm text-bunker-300">
|
||||||
|
Reminder every {secretReminderRepeatDays}{" "}
|
||||||
|
{secretReminderRepeatDays > 1 ? "days" : "day"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="px-2 py-1"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2 ml-1 flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
className="px-2 py-1"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
|
>
|
||||||
|
Create Reminder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Comments & Notes">
|
||||||
|
<TextArea
|
||||||
|
className="border border-mineshaft-600 text-sm"
|
||||||
|
{...register("comment")}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="skipMultilineEncoding"
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Switch
|
||||||
|
id="skipmultiencoding-option"
|
||||||
|
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
||||||
|
isChecked={!value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="items-center"
|
||||||
|
>
|
||||||
|
Enable multi line encoding
|
||||||
|
<Tooltip
|
||||||
|
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||||
|
<div className="mb-2">Version History</div>
|
||||||
|
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||||
|
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||||
|
<div key={id} className="flex flex-col space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||||
|
</div>
|
||||||
|
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||||
|
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||||
|
<div className="break-all font-mono">{value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
isFullWidth
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</DrawerContent>
|
||||||
</DrawerContent>
|
</Drawer>
|
||||||
</Drawer>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,9 +111,11 @@ export const SecretItem = memo(
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
const overrideAction = watch("overrideAction");
|
const overrideAction = watch("overrideAction");
|
||||||
const hasComment = Boolean(watch("comment"));
|
const hasComment = Boolean(watch("comment"));
|
||||||
const hasReminder = Boolean(watch("reminderRepeatDays"));
|
|
||||||
|
|
||||||
const selectedTags = watch("tags", []);
|
const selectedTags = watch("tags", []);
|
||||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||||
@@ -191,6 +193,8 @@ export const SecretItem = memo(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
|
repeatDays={secretReminderRepeatDays}
|
||||||
|
note={secretReminderNote}
|
||||||
isOpen={createReminderFormOpen}
|
isOpen={createReminderFormOpen}
|
||||||
onOpenChange={(_, data) => {
|
onOpenChange={(_, data) => {
|
||||||
setCreateReminderFormOpen.toggle();
|
setCreateReminderFormOpen.toggle();
|
||||||
@@ -380,22 +384,24 @@ export const SecretItem = memo(
|
|||||||
<IconButton
|
<IconButton
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||||
hasReminder && "w-5 text-primary"
|
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
|
||||||
)}
|
)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="md"
|
size="md"
|
||||||
ariaLabel="add-reminder"
|
ariaLabel="add-reminder"
|
||||||
>
|
>
|
||||||
<Tooltip content="Reminder">
|
<Tooltip
|
||||||
|
content={
|
||||||
|
secretReminderRepeatDays && secretReminderRepeatDays > 0
|
||||||
|
? `Every ${secretReminderRepeatDays} day${
|
||||||
|
Number(secretReminderRepeatDays) > 1 ? "s" : ""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: "Reminder"
|
||||||
|
}
|
||||||
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
onClick={() => {
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
if (!hasReminder) {
|
|
||||||
setCreateReminderFormOpen.on();
|
|
||||||
} else {
|
|
||||||
setValue("reminderRepeatDays", null, { shouldDirty: true });
|
|
||||||
setValue("reminderNote", null, { shouldDirty: true });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
icon={faClock}
|
icon={faClock}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -170,7 +170,11 @@ export const AddAPIKeyModal = ({
|
|||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("addAPIKey", false)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -139,7 +139,7 @@ export const WebhooksTab = withProjectPermission(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
|
|||||||
Reference in New Issue
Block a user