Compare commits
139 Commits
daniel/ui-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
c8c5caba62 | ||
|
f408a6f60c | ||
|
391ed0ed74 | ||
|
aef40212d2 | ||
|
5aa7cd46c1 | ||
|
6c0b916ad8 | ||
|
d7bc80308d | ||
|
b7c7b242e8 | ||
|
37827367ed | ||
|
403b1ce993 | ||
|
c3c0006a25 | ||
|
2241908d0a | ||
|
59b822510c | ||
|
d1408aff35 | ||
|
c67084f08d | ||
|
a280e002ed | ||
|
76c4a8660f | ||
|
8c54dd611e | ||
|
5c75f526e7 | ||
|
113e777b25 | ||
|
2a93449ffe | ||
|
1ef1c042da | ||
|
b64672a921 | ||
|
227e013502 | ||
|
44ca8c315e | ||
|
7766a7f4dd | ||
|
3cb150a749 | ||
|
9e9ce261c8 | ||
|
fab7167850 | ||
|
c7de9aab4e | ||
|
3560346f85 | ||
|
f0bf2f8dd0 | ||
|
2a6216b8fc | ||
|
c05230f667 | ||
|
d68055a264 | ||
|
dc6056b564 | ||
|
94f0811661 | ||
|
7b84ae6173 | ||
|
5710a304f8 | ||
|
91e3bbba34 | ||
|
02112ede07 | ||
|
08cfbf64e4 | ||
|
18da522b45 | ||
|
8cf68fbd9c | ||
|
d6b82dfaa4 | ||
|
7bd4eed328 | ||
|
0341c32da0 | ||
|
caea055281 | ||
|
c08c78de8d | ||
|
3765a14246 | ||
|
c5a11e839b | ||
|
93bd3d8270 | ||
|
b9601dd418 | ||
|
ae3bc04b07 | ||
|
11edefa66f | ||
|
f71459ede0 | ||
|
33324a5a3c | ||
|
5c6781a705 | ||
|
71e31518d7 | ||
|
f6f6db2898 | ||
|
55780b65d3 | ||
|
83bbf9599d | ||
|
f8f2b2574d | ||
|
318d12addd | ||
|
872a28d02a | ||
|
6f53a5631c | ||
|
ff2098408d | ||
|
9e85d9bbf0 | ||
|
0f3a48bb32 | ||
|
f869def8ea | ||
|
378bc57a88 | ||
|
242179598b | ||
|
e3e049b66c | ||
|
878e4a79e7 | ||
|
609ce8e5cc | ||
|
04c1ea9b11 | ||
|
3baca73e53 | ||
|
36adf6863b | ||
|
6363e7d30a | ||
|
f9621fad8e | ||
|
90be28b87a | ||
|
671adee4d7 | ||
|
c9cb90c98e | ||
|
9f691df395 | ||
|
d702a61586 | ||
|
1c16f406a7 | ||
|
90f739caa6 | ||
|
ede8b6f286 | ||
|
232c547d75 | ||
|
fe08bbb691 | ||
|
2bd06ecde4 | ||
|
08b79d65ea | ||
|
4e1733ba6c | ||
|
a4e495ea1c | ||
|
a750d68363 | ||
|
d7161a353d | ||
|
12c414817f | ||
|
e5e494d0ee | ||
|
5a21b85e9e | ||
|
348fdf6429 | ||
|
88e609cb66 | ||
|
78058d691a | ||
|
1d465a50c3 | ||
|
ffc7249c7c | ||
|
90bcf23097 | ||
|
5fa4d9029d | ||
|
7160cf58ee | ||
|
6b2d757e39 | ||
|
c075fcceca | ||
|
e25f5dd65f | ||
|
3eef023c30 | ||
|
e63deb0860 | ||
|
02b2851990 | ||
|
cb828200e1 | ||
|
77d068ae2c | ||
|
8702af671d | ||
|
31c0fd96ea | ||
|
2c539697df | ||
|
ae97b74933 | ||
|
3e6af2dae5 | ||
|
3c91e1127f | ||
|
0e31a9146a | ||
|
d2a93eb1d2 | ||
|
fa1b28b33f | ||
|
415cf31b2d | ||
|
9002e6cb33 | ||
|
1ede551c3e | ||
|
b7b43858f6 | ||
|
c91789e6d0 | ||
|
db0ba4be10 | ||
|
f73c807aa0 | ||
|
203e00216f | ||
|
ee215bccfa | ||
|
7a3a6663f1 | ||
|
8c491668dc | ||
|
c873e2cba8 | ||
|
1bc045a7fa | ||
|
533de93199 | ||
|
115b4664bf |
@@ -3,9 +3,6 @@
|
|||||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
# Required
|
|
||||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
@@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
|
|||||||
POSTGRES_USER=infisical
|
POSTGRES_USER=infisical
|
||||||
POSTGRES_DB=infisical
|
POSTGRES_DB=infisical
|
||||||
|
|
||||||
|
# Required
|
||||||
|
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
149
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
name: Deployment pipeline
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
infisical-image:
|
||||||
|
name: Build backend image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
load: true
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
|
tags: infisical/infisical:test
|
||||||
|
- name: 🏗️ Build backend and push to docker hub
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
|
tags: |
|
||||||
|
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/staging_infisical:latest
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
|
||||||
|
|
||||||
|
gamma-deployment:
|
||||||
|
name: Deploy to gamma
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-image]
|
||||||
|
environment:
|
||||||
|
name: Gamma
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Change directory to backend and install dependencies
|
||||||
|
env:
|
||||||
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run migration:latest
|
||||||
|
- name: Configure AWS Credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
audience: sts.amazonaws.com
|
||||||
|
aws-region: us-east-1
|
||||||
|
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: Download task definition
|
||||||
|
run: |
|
||||||
|
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||||
|
- name: Render Amazon ECS task definition
|
||||||
|
id: render-web-container
|
||||||
|
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: task-definition.json
|
||||||
|
container-name: infisical-prod-platform
|
||||||
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
environment-variables: "LOG_LEVEL=info"
|
||||||
|
- name: Deploy to Amazon ECS service
|
||||||
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
|
service: infisical-prod-platform
|
||||||
|
cluster: infisical-prod-platform
|
||||||
|
wait-for-service-stability: true
|
||||||
|
|
||||||
|
production-postgres-migration:
|
||||||
|
name: Deploy to production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [gamma-deployment]
|
||||||
|
environment:
|
||||||
|
name: Production
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Change directory to backend and install dependencies
|
||||||
|
env:
|
||||||
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run migration:latest
|
||||||
|
- name: Configure AWS Credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
audience: sts.amazonaws.com
|
||||||
|
aws-region: us-east-1
|
||||||
|
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: Download task definition
|
||||||
|
run: |
|
||||||
|
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||||
|
- name: Render Amazon ECS task definition
|
||||||
|
id: render-web-container
|
||||||
|
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: task-definition.json
|
||||||
|
container-name: infisical-prod-platform
|
||||||
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
environment-variables: "LOG_LEVEL=info"
|
||||||
|
- name: Deploy to Amazon ECS service
|
||||||
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
|
service: infisical-prod-platform
|
||||||
|
cluster: infisical-prod-platform
|
||||||
|
wait-for-service-stability: true
|
22
.github/workflows/build-staging-and-deploy.yml
vendored
@@ -8,6 +8,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_FOR_ECR }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_FOR_ECR }}
|
||||||
|
aws-region: us-east-1
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v1
|
||||||
- name: 📦 Install dependencies to test all dependencies
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
run: npm ci --only-production
|
run: npm ci --only-production
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
@@ -35,16 +44,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: Dockerfile.standalone-infisical
|
file: Dockerfile.standalone-infisical
|
||||||
tags: infisical/infisical:test
|
tags: infisical/infisical:test
|
||||||
# - name: ⏻ Spawn backend container and dependencies
|
- name: 🏗️ Build backend and push to docker hub
|
||||||
# run: |
|
|
||||||
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
|
||||||
# - name: 🧪 Test backend image
|
|
||||||
# run: |
|
|
||||||
# ./.github/resources/healthcheck.sh infisical-backend-test
|
|
||||||
# - name: ⏻ Shut down backend container and dependencies
|
|
||||||
# run: |
|
|
||||||
# docker compose -f .github/resources/docker-compose.be-test.yml down
|
|
||||||
- name: 🏗️ Build backend and push
|
|
||||||
uses: depot/build-push-action@v1
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
project: 64mmf0n610
|
project: 64mmf0n610
|
||||||
@@ -59,6 +59,8 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
|
||||||
|
|
||||||
postgres-migration:
|
postgres-migration:
|
||||||
name: Run latest migration files
|
name: Run latest migration files
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@@ -118,9 +118,6 @@ WORKDIR /backend
|
|||||||
|
|
||||||
ENV TELEMETRY_ENABLED true
|
ENV TELEMETRY_ENABLED true
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
|
||||||
CMD node healthcheck.js
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
|
@@ -10,7 +10,8 @@
|
|||||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||||
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||||
<a href="https://www.infisical.com">Website</a>
|
<a href="https://www.infisical.com">Website</a> |
|
||||||
|
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@@ -19,7 +19,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.refine(
|
||||||
(val) => Object.keys(OrgMembershipRole).includes(val),
|
(val) => !Object.keys(OrgMembershipRole).includes(val),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((v) => slugify(v) === v, {
|
.refine((v) => slugify(v) === v, {
|
||||||
|
@@ -9,6 +9,7 @@ import jmespath from "jmespath";
|
|||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { getDbConnectionHost } from "@app/lib/knex";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||||
@@ -89,7 +90,7 @@ export const secretRotationDbFn = async ({
|
|||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||||
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
|
if (host === "localhost" || host === "127.0.0.1" || getDbConnectionHost(appCfg.DB_CONNECTION_URI) === host)
|
||||||
throw new Error("Invalid db host");
|
throw new Error("Invalid db host");
|
||||||
|
|
||||||
const db = knex({
|
const db = knex({
|
||||||
|
11
backend/src/lib/knex/connection.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { URL } from "url"; // Import the URL class
|
||||||
|
|
||||||
|
export const getDbConnectionHost = (urlString: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
// Split hostname and port (if provided)
|
||||||
|
return url.hostname.split(":")[0];
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
@@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables";
|
|||||||
|
|
||||||
import { DatabaseError } from "../errors";
|
import { DatabaseError } from "../errors";
|
||||||
|
|
||||||
|
export * from "./connection";
|
||||||
export * from "./join";
|
export * from "./join";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
|
|
||||||
|
@@ -14,13 +14,13 @@ export const fastifySwagger = fp(async (fastify) => {
|
|||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
|
||||||
url: "http://localhost:8080",
|
|
||||||
description: "Local server"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
url: "https://app.infisical.com",
|
url: "https://app.infisical.com",
|
||||||
description: "Production server"
|
description: "Production server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "http://localhost:8080",
|
||||||
|
description: "Local server"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
@@ -150,8 +150,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
message: "Slug must be a valid slug"
|
message: "Slug must be a valid slug"
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.CREATE.slug),
|
.describe(PROJECTS.CREATE.slug)
|
||||||
organizationSlug: z.string().trim().describe(PROJECTS.CREATE.organizationSlug)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -166,7 +165,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orgSlug: req.body.organizationSlug,
|
|
||||||
workspaceName: req.body.projectName,
|
workspaceName: req.body.projectName,
|
||||||
slug: req.body.slug
|
slug: req.body.slug
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -92,7 +92,6 @@ export const projectServiceFactory = ({
|
|||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
* */
|
* */
|
||||||
const createProject = async ({
|
const createProject = async ({
|
||||||
orgSlug,
|
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -100,13 +99,7 @@ export const projectServiceFactory = ({
|
|||||||
workspaceName,
|
workspaceName,
|
||||||
slug: projectSlug
|
slug: projectSlug
|
||||||
}: TCreateProjectDTO) => {
|
}: TCreateProjectDTO) => {
|
||||||
if (!orgSlug) {
|
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Must provide organization slug to create project"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await orgDAL.findOne({ slug: orgSlug });
|
|
||||||
|
|
||||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -291,10 +284,11 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
// Get the role permission for the identity
|
// Get the role permission for the identity
|
||||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
ProjectMembershipRole.Admin,
|
OrgMembershipRole.Member,
|
||||||
organization.id
|
organization.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Identity has to be at least a member in order to create projects
|
||||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
if (!hasPrivilege)
|
if (!hasPrivilege)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
@@ -24,7 +24,6 @@ export type TCreateProjectDTO = {
|
|||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
orgSlug: string;
|
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
};
|
};
|
||||||
|
@@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
if (!parentFolder) return [];
|
if (!parentFolder) return [];
|
||||||
|
|
||||||
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
|
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
|
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
|
||||||
var secretsResponse GetEncryptedSecretsV3Response
|
var secretsResponse GetEncryptedSecretsV3Response
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
R().
|
R().
|
||||||
SetResult(&secretsResponse).
|
SetResult(&secretsResponse).
|
||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
SetBody(request).
|
SetBody(request).
|
||||||
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
|
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
||||||
|
@@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSecretByNameV3Request struct {
|
type UpdateSecretByNameV3Request struct {
|
||||||
SecretName string `json:"secretName"`
|
|
||||||
WorkspaceID string `json:"workspaceId"`
|
WorkspaceID string `json:"workspaceId"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
||||||
WorkspaceID: workspaceFile.WorkspaceId,
|
WorkspaceID: workspaceFile.WorkspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
SecretName: secret.PlainTextKey,
|
|
||||||
SecretValueCiphertext: secret.SecretValueCiphertext,
|
SecretValueCiphertext: secret.SecretValueCiphertext,
|
||||||
SecretValueIV: secret.SecretValueIV,
|
SecretValueIV: secret.SecretValueIV,
|
||||||
SecretValueTag: secret.SecretValueTag,
|
SecretValueTag: secret.SecretValueTag,
|
||||||
@@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
|
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to process secret update request")
|
util.HandleError(err, "Unable to process secret update request")
|
||||||
return
|
return
|
||||||
@@ -419,7 +418,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
@@ -477,7 +476,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
|
@@ -1,37 +1,102 @@
|
|||||||
---
|
---
|
||||||
title: "MySQL/MariaDB"
|
title: "MySQL/MariaDB"
|
||||||
description: "Rotated database user password of a MySQL or MariaDB"
|
description: "How to rotate MySQL/MariaDB database user passwords"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical will update periodically the provided database user's password.
|
The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval.
|
||||||
|
|
||||||
<Warning>
|
|
||||||
At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. User's has to create the two user's for Infisical to rotate and provide them required database access
|
1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
2. Infisical will connect with your database with admin access
|
2. Create another MySQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated
|
|
||||||
5. Update it's password with random value
|
To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
|
||||||
6. After testing it gets saved to the provided secret mapping
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Infisical connects to your database using the provided `admin` user account.
|
||||||
|
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||||
|
3. The new password is then tested by logging into the database
|
||||||
|
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||||
|
5. The process is then repeated for `user-b` on the next rotation.
|
||||||
|
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `MySQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on MySQL card" />
|
||||||
- Host: DB host
|
<Step title="Provide the inputs">
|
||||||
- Port: DB port(number)
|
<ParamField path="Admin Username" type="string" required>
|
||||||
- Username1: The first username in two to rotate
|
Rotator admin username
|
||||||
- Username2: The second username in two to rotate
|
</ParamField>
|
||||||
- CA: Certificate to connect with database(string)
|
|
||||||
4. Final step
|
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Congrats. You have 10x your MySQL/MariaDB access security.
|
<ParamField path="Admin password" type="string" required>
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required>
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required>
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required>
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required>
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string">
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||||
|
|
||||||
|
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||||
|
|
||||||
|
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||||
|
|
||||||
|
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
@@ -1,33 +1,104 @@
|
|||||||
---
|
---
|
||||||
title: "PostgreSQL/CockroachDB"
|
title: "PostgreSQL/CockroachDB"
|
||||||
description: "Rotated database user password of a PostgreSQL or Cockroach DB"
|
description: "How to rotate postgreSQL/cockroach database user passwords"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical will update periodically the provided database user's password.
|
The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval.
|
||||||
|
|
||||||
## Working
|
|
||||||
|
|
||||||
1. User's has to create the two user's for Infisical to rotate and provide them required database access.
|
## Prerequisite
|
||||||
2. Infisical will connect with your database with admin access.
|
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated.
|
1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
5. Update it's password with random value.
|
2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
|
||||||
6. After testing it gets saved to the provided secret mapping.
|
|
||||||
|
To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html).
|
||||||
|
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Infisical connects to your database using the provided `admin` user account.
|
||||||
|
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||||
|
3. The new password is then tested by logging into the database
|
||||||
|
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||||
|
5. The process is then repeated for `user-b` on the next rotation.
|
||||||
|
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `PostgreSQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on PostgresSQL card" />
|
||||||
- Host: DB host
|
|
||||||
- Port: DB port(number)
|
|
||||||
- Username1: The first username in two to rotate
|
|
||||||
- Username2: The second username in two to rotate
|
|
||||||
- CA: Certificate to connect with database(string)
|
|
||||||
4. Final step
|
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Congratulations. You have improved your PostgreSQL/CockroachDB access security.
|
<Step title="Provide the inputs">
|
||||||
|
<ParamField path="Admin Username" type="string" required="true">
|
||||||
|
Rotator admin username
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Admin password" type="string" required="true">
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required="true">
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required="true">
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required="true">
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required="true">
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string" optional>
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||||
|
|
||||||
|
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||||
|
|
||||||
|
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||||
|
|
||||||
|
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
@@ -1,31 +1,58 @@
|
|||||||
---
|
---
|
||||||
title: "Twilio SendGrid"
|
title: "Twilio SendGrid"
|
||||||
description: "Rotate Twilio SendGrid API keys"
|
description: "How to rotate Twilio SendGrid API keys"
|
||||||
---
|
---
|
||||||
|
|
||||||
Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails.
|
Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
|
||||||
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. Infisical will need an admin token of SendGrid to create API keys dynamically.
|
You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
|
||||||
2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically
|
|
||||||
3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys)
|
Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Using the provided admin API key, Infisical will attempt to create child API keys with the specified permissions.
|
||||||
|
New keys will ge generated every time a rotation occurs. Behind the scenes, Infisical uses the [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) to generate new API keys.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `Twilio SendGrid Card`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin API Key:
|
</Step>
|
||||||
SendGrid admin key to create lower scoped API keys.
|
<Step title="Click on Twilio SendGrid Card" />
|
||||||
- API Key Scopes
|
<Step title="Provide the inputs">
|
||||||
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
|
<ParamField path="Admin API Key" type="string" required>
|
||||||
|
SendGrid admin API key with permission to create child scoped API keys.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
4. Final step
|
<ParamField path="Admin API Key" type="array" required>
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
The permissions that the newly generated API keys will have. To view possible permissions, visit [this documentation](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions).
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
Permissions must be entered as a list of strings.
|
||||||
- Your done and good to go.
|
|
||||||
|
Example: `["user.profile.read", "user.profile.update"]`
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="API KEY" type="string" required>
|
||||||
|
Select an existing select key where the newly rotated API key will get saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
Now your output mapped secret value will be replaced periodically by SendGrid.
|
Now your output mapped secret value will be replaced periodically by SendGrid.
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 733 KiB After Width: | Height: | Size: 739 KiB |
BIN
docs/images/secret-rotation/mysql-step1.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/images/secret-rotation/postgres-step1.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/secret-rotation/postgres-step2.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/images/secret-rotation/sendgrid-step1.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/images/secret-rotation/sendgrid-step2.png
Normal file
After Width: | Height: | Size: 52 KiB |
@@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
|
|||||||
|
|
||||||
## Install Operator
|
## Install Operator
|
||||||
|
|
||||||
The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Helm (recommended)">
|
<Tab title="Helm (recommended)">
|
||||||
@@ -61,23 +61,38 @@ Once you have installed the operator to your cluster, you'll need to create a `I
|
|||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
metadata:
|
||||||
# Name of of this InfisicalSecret resource
|
|
||||||
name: infisicalsecret-sample
|
name: infisicalsecret-sample
|
||||||
|
labels:
|
||||||
|
label-to-be-passed-to-managed-secret: sample-value
|
||||||
|
annotations:
|
||||||
|
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||||
spec:
|
spec:
|
||||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
|
||||||
hostAPI: https://app.infisical.com/api
|
hostAPI: https://app.infisical.com/api
|
||||||
resyncInterval: 60
|
resyncInterval: 10
|
||||||
authentication:
|
authentication:
|
||||||
|
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||||
|
# If you have multiple authentication methods defined, it may cause issues.
|
||||||
|
universalAuth:
|
||||||
|
secretsScope:
|
||||||
|
projectSlug: <project-slug>
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials
|
||||||
|
secretNamespace: default
|
||||||
|
|
||||||
serviceToken:
|
serviceToken:
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token
|
secretName: service-token
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
secretsScope:
|
secretsScope:
|
||||||
envSlug: dev
|
envSlug: <env-slug>
|
||||||
secretsPath: "/"
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
|
|
||||||
managedSecretReference:
|
managedSecretReference:
|
||||||
secretName: managed-secret # <-- the name of kubernetes secret that will be created
|
secretName: managed-secret
|
||||||
secretNamespace: default # <-- where the kubernetes secret should be created
|
secretNamespace: default
|
||||||
|
# secretType: kubernetes.io/dockerconfigjson
|
||||||
```
|
```
|
||||||
### InfisicalSecret CRD properties
|
### InfisicalSecret CRD properties
|
||||||
|
|
||||||
@@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication">
|
<Accordion title="authentication">
|
||||||
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical.
|
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.serviceTokenSecretReference">
|
<Accordion title="authentication.universalAuth">
|
||||||
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token.
|
The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a machine identity">
|
||||||
|
You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth).
|
||||||
|
</Step>
|
||||||
|
<Step title="Create Kubernetes secret containing machine identity credentials">
|
||||||
|
Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials.
|
||||||
|
To quickly create a Kubernetes secret containing the identity credentials, you can run the command below.
|
||||||
|
|
||||||
|
Make sure you replace `<your-identity-client-id>` with the identity client ID and `<your-identity-client-secret>` with the identity client secret.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
kubectl create secret generic universal-auth-credentials --from-literal=clientId="<your-identity-client-id>" --from-literal=clientSecret="<your-identity-client-secret>"
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add reference for the Kubernetes secret containing the identity credentials">
|
||||||
|
Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Make sure to also populate the `secretsScope` field with the project slug _`projectSlug`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```yaml
|
||||||
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
|
kind: InfisicalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisicalsecret-sample-crd
|
||||||
|
spec:
|
||||||
|
authentication:
|
||||||
|
universalAuth:
|
||||||
|
secretsScope:
|
||||||
|
projectSlug: <project-slug> # <-- project slug
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials
|
||||||
|
secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials
|
||||||
|
...
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="authentication.serviceToken">
|
||||||
|
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token.
|
||||||
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
|
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
|
||||||
|
|
||||||
#### 1. Generate service token
|
#### 1. Generate service token
|
||||||
@@ -122,13 +186,17 @@ Default re-sync interval is every 1 minute.
|
|||||||
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
kubectl create secret generic service-token --from-literal=infisicalToken="<your-service-token-here>"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Add reference for the Kubernetes secret containing service token
|
#### 3. Add reference for the Kubernetes secret containing service token
|
||||||
|
|
||||||
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Make sure to also populate the `secretsScope` field with the, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
|
||||||
|
</Info>
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
@@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
|
|||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
||||||
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
||||||
|
secretsScope:
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.secretsScope">
|
|
||||||
This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
|
|
||||||
A scope is defined by `envSlug` and `secretsPath`.
|
|
||||||
|
|
||||||
#### envSlug
|
|
||||||
|
|
||||||
This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
|
|
||||||
|
|
||||||
#### secretsPath
|
|
||||||
|
|
||||||
secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
|
|
||||||
|
|
||||||
Both fields are required.
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="managedSecretReference">
|
<Accordion title="managedSecretReference">
|
||||||
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
||||||
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
||||||
|
@@ -9,7 +9,7 @@ This guide walks through how you can use these paid features in Infisical.
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Purchase a license">
|
<Step title="Purchase a license">
|
||||||
Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting team@infisical.com to purchase a license.
|
Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting sales@infisical.com to purchase a license.
|
||||||
|
|
||||||
Once purchased, you will be issued a license key.
|
Once purchased, you will be issued a license key.
|
||||||
</Step>
|
</Step>
|
||||||
|
@@ -29,6 +29,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: ["react", "prettier", "simple-import-sort", "import"],
|
plugins: ["react", "prettier", "simple-import-sort", "import"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
quotes: ["error", "double", { avoidEscape: true }],
|
quotes: ["error", "double", { avoidEscape: true }],
|
||||||
"comma-dangle": ["error", "only-multiline"],
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
@@ -72,7 +73,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"simple-import-sort/exports": "warn",
|
"simple-import-sort/exports": "warn",
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"simple-import-sort/imports": [
|
"simple-import-sort/imports": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
@@ -1,28 +1,28 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
addons: [
|
addons: [
|
||||||
'@storybook/addon-links',
|
"@storybook/addon-links",
|
||||||
'@storybook/addon-essentials',
|
"@storybook/addon-essentials",
|
||||||
'@storybook/addon-interactions',
|
"@storybook/addon-interactions",
|
||||||
'storybook-dark-mode',
|
"storybook-dark-mode",
|
||||||
{
|
{
|
||||||
name: '@storybook/addon-styling',
|
name: "@storybook/addon-styling",
|
||||||
options: {
|
options: {
|
||||||
postCss: {
|
postCss: {
|
||||||
implementation: require('postcss')
|
implementation: require("postcss")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: '@storybook/nextjs',
|
name: "@storybook/nextjs",
|
||||||
options: {}
|
options: {}
|
||||||
},
|
},
|
||||||
core: {
|
core: {
|
||||||
disableTelemetry: true
|
disableTelemetry: true
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
autodocs: 'tag'
|
autodocs: "tag"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config";
|
|||||||
|
|
||||||
export const initPostHog = () => {
|
export const initPostHog = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
console.log("Hi there 👋")
|
console.log("Hi there 👋");
|
||||||
try {
|
try {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -19,7 +19,7 @@ export const initPostHog = () => {
|
|||||||
|
|
||||||
return posthog;
|
return posthog;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("posthog err", e)
|
console.log("posthog err", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
const Error = ({ text }: { text: string }): JSX.Element => {
|
const Error = ({ text }: { text: string }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
<div className="relative m-auto flex w-fit flex-row items-center justify-center rounded-full">
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
|
<FontAwesomeIcon icon={faExclamationTriangle} className="mx-2 mt-1.5 mb-2 text-red" />
|
||||||
{text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
|
{text && <p className="relative top-0 mr-2 py-1 text-sm text-red">{text}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -39,16 +39,16 @@ const InputField = ({
|
|||||||
|
|
||||||
if (isStatic === true) {
|
if (isStatic === true) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
|
<div className="my-2 flex w-full max-w-md flex-col justify-center md:my-4">
|
||||||
<p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
|
<p className="mb-0.5 text-sm font-semibold text-gray-400">{label}</p>
|
||||||
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
|
{text && <p className="mb-2 text-xs text-gray-400">{text}</p>}
|
||||||
<input
|
<input
|
||||||
onChange={(e) => onChangeHandler(e.target.value)}
|
onChange={(e) => onChangeHandler(e.target.value)}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
|
className="text-md min-w-16 w-full rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
|
||||||
name={name}
|
name={name}
|
||||||
readOnly
|
readOnly
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
@@ -58,12 +58,12 @@ const InputField = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="w-full flex-col">
|
||||||
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
|
<div className="mb-0.5 flex flex-row items-center text-mineshaft-300">
|
||||||
<p className="text-sm font-semibold mr-1">{label}</p>
|
<p className="mr-1 text-sm font-semibold">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
className={`group relative flex w-full max-w-2xl flex-col justify-center border ${
|
||||||
error ? "border-red" : "border-mineshaft-500"
|
error ? "border-red" : "border-mineshaft-500"
|
||||||
} rounded-md`}
|
} rounded-md`}
|
||||||
>
|
>
|
||||||
@@ -75,11 +75,11 @@ const InputField = ({
|
|||||||
required={isRequired}
|
required={isRequired}
|
||||||
className={`${
|
className={`${
|
||||||
blurred
|
blurred
|
||||||
? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400"
|
? "text-bunker-800 focus:text-gray-400 active:text-gray-400 group-hover:text-gray-400"
|
||||||
: ""
|
: ""
|
||||||
} ${
|
} ${
|
||||||
error ? "focus:ring-red/50" : "focus:ring-primary/50"
|
error ? "focus:ring-red/50" : "focus:ring-primary/50"
|
||||||
} relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
} text-md min-w-16 peer relative w-full rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
|
||||||
name={name}
|
name={name}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
@@ -91,7 +91,7 @@ const InputField = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordVisible(!passwordVisible);
|
setPasswordVisible(!passwordVisible);
|
||||||
}}
|
}}
|
||||||
className="absolute self-end mr-3 text-gray-400 cursor-pointer"
|
className="absolute mr-3 cursor-pointer self-end text-gray-400"
|
||||||
>
|
>
|
||||||
{passwordVisible ? (
|
{passwordVisible ? (
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
@@ -101,7 +101,7 @@ const InputField = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{blurred && (
|
{blurred && (
|
||||||
<div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
|
<div className="peer absolute flex h-10 w-fit max-w-xl items-center overflow-hidden text-clip rounded-md text-gray-400/50 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible">
|
||||||
<p className="ml-2" />
|
<p className="ml-2" />
|
||||||
{value
|
{value
|
||||||
.split("")
|
.split("")
|
||||||
@@ -109,7 +109,7 @@ const InputField = ({
|
|||||||
.map(() => (
|
.map(() => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
className="text-xxs mx-0.5"
|
className="mx-0.5 text-xxs"
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -121,7 +121,7 @@ const InputField = ({
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">{errorText}</p>}
|
{error && <p className="mx-0 mt-0.5 mb-2 max-w-xs text-xs text-red">{errorText}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -34,19 +34,19 @@ const ListBox = ({
|
|||||||
<Listbox value={isSelected} onChange={onChange}>
|
<Listbox value={isSelected} onChange={onChange}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`text-gray-400 relative ${
|
className={`relative text-gray-400 ${
|
||||||
isFull ? "w-full" : "w-52"
|
isFull ? "w-full" : "w-52"
|
||||||
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
} focus-visible:ring-offset-orange-300 cursor-default rounded-md bg-white/[0.07] py-2.5 pl-3 pr-10 text-left shadow-md duration-200 hover:bg-white/[0.11] focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{text}
|
{text}
|
||||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
<span className="ml-1 block cursor-pointer truncate font-semibold text-gray-300">
|
||||||
{" "}
|
{" "}
|
||||||
{isSelected}
|
{isSelected}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data && (
|
{data && (
|
||||||
<div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex cursor-pointer items-center pr-2">
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -58,16 +58,16 @@ const ListBox = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="border border-mineshaft-700 z-[70] p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm no-scrollbar no-scrollbar::-webkit-scrollbar">
|
<Listbox.Options className="no-scrollbar::-webkit-scrollbar absolute z-[70] mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-700 bg-bunker p-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 no-scrollbar focus:outline-none sm:text-sm">
|
||||||
{data.map((person, personIdx) => (
|
{data.map((person, personIdx) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={`${person}.${personIdx + 1}`}
|
key={`${person}.${personIdx + 1}`}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
`relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${
|
||||||
selected ? "bg-white/10 text-gray-400 font-bold" : ""
|
selected ? "bg-white/10 font-bold text-gray-400" : ""
|
||||||
} ${
|
} ${
|
||||||
active && !selected
|
active && !selected
|
||||||
? "bg-white/5 text-mineshaft-200 cursor-pointer"
|
? "cursor-pointer bg-white/5 text-mineshaft-200"
|
||||||
: "text-gray-400"
|
: "text-gray-400"
|
||||||
} `
|
} `
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ const ListBox = ({
|
|||||||
{person}
|
{person}
|
||||||
</span>
|
</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
|
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 text-primary">
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -92,9 +92,9 @@ const ListBox = ({
|
|||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
{buttonAction && (
|
{buttonAction && (
|
||||||
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
|
<button type="button" onClick={buttonAction} className="w-full cursor-pointer">
|
||||||
<div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
|
<div className="relative my-0.5 mt-2 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-lime-300 hover:font-semibold hover:text-black">
|
||||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
|
||||||
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||||
</span>
|
</span>
|
||||||
Add Project
|
Add Project
|
||||||
|
@@ -43,7 +43,7 @@ const Button = ({
|
|||||||
loading,
|
loading,
|
||||||
icon,
|
icon,
|
||||||
iconDisabled,
|
iconDisabled,
|
||||||
type = "button",
|
type = "button"
|
||||||
}: ButtonProps): JSX.Element => {
|
}: ButtonProps): JSX.Element => {
|
||||||
// Check if the button show always be 'active' - then true;
|
// Check if the button show always be 'active' - then true;
|
||||||
// or if it should switch between 'active' and 'disabled' - then give the status
|
// or if it should switch between 'active' and 'disabled' - then give the status
|
||||||
@@ -53,9 +53,13 @@ const Button = ({
|
|||||||
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
||||||
|
|
||||||
// Setting background colors and hover modes
|
// Setting background colors and hover modes
|
||||||
color === "mineshaft" && activityStatus && "bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60",
|
color === "mineshaft" &&
|
||||||
|
activityStatus &&
|
||||||
|
"bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60",
|
||||||
color === "mineshaft" && !activityStatus && "bg-mineshaft",
|
color === "mineshaft" && !activityStatus && "bg-mineshaft",
|
||||||
(color === "primary" || !color) && activityStatus && "bg-primary border border-primary-400 opacity-80 hover:opacity-100",
|
(color === "primary" || !color) &&
|
||||||
|
activityStatus &&
|
||||||
|
"bg-primary border border-primary-400 opacity-80 hover:opacity-100",
|
||||||
(color === "primary" || !color) && !activityStatus && "bg-primary",
|
(color === "primary" || !color) && !activityStatus && "bg-primary",
|
||||||
color === "red" && "bg-red-800 border border-red",
|
color === "red" && "bg-red-800 border border-red",
|
||||||
|
|
||||||
@@ -78,7 +82,9 @@ const Button = ({
|
|||||||
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
|
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
|
||||||
color === "red" && "text-gray-200",
|
color === "red" && "text-gray-200",
|
||||||
color === "none" && "text-gray-200 text-xl",
|
color === "none" && "text-gray-200 text-xl",
|
||||||
activityStatus && color !== "red" && color !== "mineshaft" && color !== "none" ? "group-hover:text-black" : "",
|
activityStatus && color !== "red" && color !== "mineshaft" && color !== "none"
|
||||||
|
? "group-hover:text-black"
|
||||||
|
: "",
|
||||||
|
|
||||||
size === "icon" && "flex items-center justify-center"
|
size === "icon" && "flex items-center justify-center"
|
||||||
);
|
);
|
||||||
@@ -103,7 +109,7 @@ const Button = ({
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
loading === true ? "opacity-100" : "opacity-0"
|
loading === true ? "opacity-100" : "opacity-0"
|
||||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
} absolute flex w-full items-center bg-primary px-3 duration-200`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loadingblack.gif"
|
src="/images/loading/loadingblack.gif"
|
||||||
@@ -116,7 +122,7 @@ const Button = ({
|
|||||||
{icon && (
|
{icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={`flex my-auto font-extrabold ${size === "icon-sm" ? "text-sm" : "text-sm"} ${
|
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-sm"} ${
|
||||||
(text || textDisabled) && "mr-2"
|
(text || textDisabled) && "mr-2"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -124,7 +130,7 @@ const Button = ({
|
|||||||
{iconDisabled && (
|
{iconDisabled && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={iconDisabled as IconProp}
|
icon={iconDisabled as IconProp}
|
||||||
className={`flex my-auto font-extrabold ${size === "icon-sm" ? "text-sm" : "text-md"} ${
|
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-md"} ${
|
||||||
(text || textDisabled) && "mr-2"
|
(text || textDisabled) && "mr-2"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
@@ -64,7 +64,7 @@ const AddProjectMemberDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="z-50 text-lg font-medium text-mineshaft-300 mb-4"
|
className="z-50 mb-4 text-lg font-medium text-mineshaft-300"
|
||||||
>
|
>
|
||||||
{t("section.members.add-dialog.already-all-invited")}
|
{t("section.members.add-dialog.already-all-invited")}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -127,7 +127,9 @@ const AddProjectMemberDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => router.push(`/org/${localStorage.getItem("orgData.id")}/members`)}
|
onButtonPressed={() =>
|
||||||
|
router.push(`/org/${localStorage.getItem("orgData.id")}/members`)
|
||||||
|
}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
text={t("section.members.add-dialog.add-user-to-org") as string}
|
text={t("section.members.add-dialog.add-user-to-org") as string}
|
||||||
size="md"
|
size="md"
|
||||||
|
@@ -28,11 +28,11 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
onCreateSubmit,
|
onCreateSubmit,
|
||||||
onEditSubmit,
|
onEditSubmit,
|
||||||
initialValues,
|
initialValues,
|
||||||
isEditMode,
|
isEditMode
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [formInput, setFormInput] = useState<FormFields>({
|
const [formInput, setFormInput] = useState<FormFields>({
|
||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
// This use effect can be removed when the unmount is happening from outside the component
|
// This use effect can be removed when the unmount is happening from outside the component
|
||||||
@@ -50,7 +50,7 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data = {
|
||||||
name: formInput.name,
|
name: formInput.name,
|
||||||
slug: formInput.slug.toLowerCase(),
|
slug: formInput.slug.toLowerCase()
|
||||||
};
|
};
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
onEditSubmit(data);
|
onEditSubmit(data);
|
||||||
@@ -62,75 +62,70 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-20' onClose={onClose}>
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-out duration-150'
|
leave="ease-out duration-150"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto z-50'>
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
{isEditMode ? "Update environment" : "Create a new environment"}
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
{isEditMode
|
|
||||||
? "Update environment"
|
|
||||||
: "Create a new environment"}
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<form onSubmit={onFormSubmit}>
|
<form onSubmit={onFormSubmit}>
|
||||||
<div className='max-h-28 mt-4'>
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Environment Name'
|
label="Environment Name"
|
||||||
onChangeHandler={(val) => onInputChange("name", val)}
|
onChangeHandler={(val) => onInputChange("name", val)}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={formInput.name}
|
value={formInput.name}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
// error={error.length > 0}
|
// error={error.length > 0}
|
||||||
// errorText={error}
|
// errorText={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='max-h-28 mt-4'>
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Environment Slug'
|
label="Environment Slug"
|
||||||
onChangeHandler={(val) => onInputChange("slug", val)}
|
onChangeHandler={(val) => onInputChange("slug", val)}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={formInput.slug}
|
value={formInput.slug}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
// error={error.length > 0}
|
// error={error.length > 0}
|
||||||
// errorText={error}
|
// errorText={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xs text-gray-500 mt-2'>
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
Slugs are shorthands used in cli to access environment
|
Slugs are shorthands used in cli to access environment
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-4 max-w-min'>
|
<div className="mt-4 max-w-min">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => null}
|
onButtonPressed={() => null}
|
||||||
type='submit'
|
type="submit"
|
||||||
color='mineshaft'
|
color="mineshaft"
|
||||||
text={isEditMode ? "Update" : "Create"}
|
text={isEditMode ? "Update" : "Create"}
|
||||||
active={formInput.name !== "" && formInput.slug !== ""}
|
active={formInput.name !== "" && formInput.slug !== ""}
|
||||||
size='md'
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -13,76 +13,63 @@ type Props = {
|
|||||||
orgName: string;
|
orgName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddUserDialog = ({
|
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
|
||||||
isOpen,
|
|
||||||
closeModal,
|
|
||||||
submitModal,
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
orgName,
|
|
||||||
}: Props) => {
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(email);
|
submitModal(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='z-50'>
|
<div className="z-50">
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-lg transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as="h3"
|
||||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||||
>
|
>
|
||||||
Invite others to {orgName}
|
Invite others to {orgName}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2 mb-4'>
|
<div className="mt-2 mb-4">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">
|
||||||
An invite is specific to an email address and expires
|
An invite is specific to an email address and expires after 1 day. For
|
||||||
after 1 day. For security reasons, you will need to
|
security reasons, you will need to separately add members to projects.
|
||||||
separately add members to projects.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='max-h-28'>
|
<div className="max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Email'
|
label="Email"
|
||||||
onChangeHandler={setEmail}
|
onChangeHandler={setEmail}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={email}
|
value={email}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4 max-w-max'>
|
<div className="mt-4 max-w-max">
|
||||||
<Button
|
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
|
||||||
onButtonPressed={submit}
|
|
||||||
color='mineshaft'
|
|
||||||
text='Invite'
|
|
||||||
size='md'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
@@ -5,7 +5,6 @@ import Button from "../buttons/Button";
|
|||||||
import InputField from "../InputField";
|
import InputField from "../InputField";
|
||||||
import { Checkbox } from "../table/Checkbox";
|
import { Checkbox } from "../table/Checkbox";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
@@ -26,8 +25,8 @@ const AddWorkspaceDialog = ({
|
|||||||
workspaceName,
|
workspaceName,
|
||||||
setWorkspaceName,
|
setWorkspaceName,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading
|
||||||
}:Props) => {
|
}: Props) => {
|
||||||
const [addAllUsers, setAddAllUsers] = useState(true);
|
const [addAllUsers, setAddAllUsers] = useState(true);
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(workspaceName, addAllUsers);
|
submitModal(workspaceName, addAllUsers);
|
||||||
@@ -60,11 +59,8 @@ const AddWorkspaceDialog = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
Create a new project
|
Create a new project
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -72,7 +68,7 @@ const AddWorkspaceDialog = ({
|
|||||||
This project will contain your secrets and configs.
|
This project will contain your secrets and configs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mt-4">
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label="Project Name"
|
label="Project Name"
|
||||||
onChangeHandler={setWorkspaceName}
|
onChangeHandler={setWorkspaceName}
|
||||||
@@ -84,10 +80,7 @@ const AddWorkspaceDialog = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 ml-1">
|
<div className="mt-4 ml-1">
|
||||||
<Checkbox
|
<Checkbox addAllUsers={addAllUsers} setAddAllUsers={setAddAllUsers} />
|
||||||
addAllUsers={addAllUsers}
|
|
||||||
setAddAllUsers={setAddAllUsers}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 max-w-min">
|
<div className="mt-4 max-w-min">
|
||||||
<Button
|
<Button
|
||||||
|
@@ -6,20 +6,14 @@ import InputField from "../InputField";
|
|||||||
// REFACTOR: Move all these modals into one reusable one
|
// REFACTOR: Move all these modals into one reusable one
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onClose: ()=>void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
onSubmit:()=>void;
|
onSubmit: () => void;
|
||||||
deleteKey?:string;
|
deleteKey?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DeleteActionModal = ({
|
const DeleteActionModal = ({ isOpen, onClose, title, onSubmit, deleteKey }: Props) => {
|
||||||
isOpen,
|
const [deleteInputField, setDeleteInputField] = useState("");
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
onSubmit,
|
|
||||||
deleteKey
|
|
||||||
}:Props) => {
|
|
||||||
const [deleteInputField, setDeleteInputField] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDeleteInputField("");
|
setDeleteInputField("");
|
||||||
@@ -28,64 +22,57 @@ const DeleteActionModal = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-150'
|
leave="ease-in duration-150"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
|
||||||
<Dialog.Title
|
|
||||||
as='h3'
|
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
>
|
||||||
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||||
This action is irrevertible.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<InputField
|
<InputField
|
||||||
isRequired
|
isRequired
|
||||||
label={`Type ${deleteKey} to delete the resource`}
|
label={`Type ${deleteKey} to delete the resource`}
|
||||||
onChangeHandler={(val) => setDeleteInputField(val)}
|
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||||
value={deleteInputField}
|
value={deleteInputField}
|
||||||
type='text'
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={
|
disabled={Boolean(deleteKey) && deleteInputField !== deleteKey}
|
||||||
Boolean(deleteKey) && deleteInputField !== deleteKey
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -5,13 +5,13 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
onSubmit: () => void
|
onSubmit: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
@@ -45,7 +45,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-600 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-mineshaft-600 bg-bunker p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
||||||
{t("dashboard:sidebar.delete-key-dialog.title")}
|
{t("dashboard:sidebar.delete-key-dialog.title")}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -57,14 +57,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
<div className="mt-6 flex justify-start">
|
<div className="mt-6 flex justify-start">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-500 opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-bunker-100 text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
className="text-semibold inline-flex justify-center rounded-md border border-transparent bg-red-500 px-4 py-2 text-sm font-medium text-bunker-100 opacity-80 duration-200 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:bg-mineshaft-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -10,66 +10,55 @@ type Props = {
|
|||||||
userIdToBeDeleted: string;
|
userIdToBeDeleted: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteUserDialog = ({
|
const DeleteUserDialog = ({ isOpen, closeModal, submitModal, userIdToBeDeleted }: Props) => {
|
||||||
isOpen,
|
|
||||||
closeModal,
|
|
||||||
submitModal,
|
|
||||||
userIdToBeDeleted,
|
|
||||||
}: Props) => {
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(userIdToBeDeleted);
|
submitModal(userIdToBeDeleted);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
Are you sure you want to remove this user from the workspace?
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
Are you sure you want to remove this user from the
|
|
||||||
workspace?
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||||
This action is irrevertible.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -34,27 +34,27 @@ const BottonRightPopup = ({
|
|||||||
}: PopupProps): JSX.Element => {
|
}: PopupProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="z-[100] drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
className="absolute bottom-0 right-0 z-[100] mr-6 mb-6 flex max-w-xl flex-col items-start rounded-md border border-gray-600/50 bg-bunker pt-3 pb-4 text-gray-200 drop-shadow-xl"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
<div className="flex w-full flex-row items-center justify-between border-b border-gray-600/70 px-6 pb-3">
|
||||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
<div className="mr-2 mt-0.5 flex flex-row text-xl font-bold">
|
||||||
<div>{titleText}</div>
|
<div>{titleText}</div>
|
||||||
<div className="ml-2.5">{emoji}</div>
|
<div className="ml-2.5">{emoji}</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faXmark}
|
icon={faXmark}
|
||||||
className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
|
className="cursor-pointer text-2xl text-gray-400 duration-200 hover:text-red"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div>
|
<div className="mt-4 mb-0.5 block px-6 text-gray-300 sm:inline">{textLine1}</div>
|
||||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
<div className="mb-4 block px-6 sm:inline">{textLine2}</div>
|
||||||
<div className="flex flex-row px-6 w-full">
|
<div className="flex w-full flex-row px-6">
|
||||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
<a
|
<a
|
||||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
className="flex w-full justify-center rounded-md bg-white/10 p-2 font-bold duration-200 hover:bg-primary hover:text-black"
|
||||||
href={buttonLink}
|
href={buttonLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
@@ -9,7 +9,7 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
|||||||
{addAllUsers === true ? (
|
{addAllUsers === true ? (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="accent-primary h-4 w-4"
|
className="h-4 w-4 accent-primary"
|
||||||
checked
|
checked
|
||||||
readOnly
|
readOnly
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
@@ -20,12 +20,12 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="add all users"
|
aria-label="add all users"
|
||||||
className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
|
className="h-4 w-4 rounded-sm border border-gray-600 bg-bunker"
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="ml-2 text-gray-500 text-sm">
|
<label className="ml-2 text-sm text-gray-500">
|
||||||
Add all members of my organization to this project.
|
Add all members of my organization to this project.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -36,25 +36,28 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full flex items-center justify-between px-6 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
|
className="pointer-events-auto relative right-3 mb-3 flex w-full items-center justify-between rounded-md border border-bunker-500 bg-mineshaft-700 px-6 py-4"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{notification.type === "error" && (
|
{notification.type === "error" && (
|
||||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md" />
|
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-red" />
|
||||||
)}
|
)}
|
||||||
{notification.type === "success" && (
|
{notification.type === "success" && (
|
||||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md" />
|
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-green" />
|
||||||
)}
|
)}
|
||||||
{notification.type === "info" && (
|
{notification.type === "info" && (
|
||||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
|
<div className="absolute top-0 left-0 h-1 w-full rounded-t-md bg-yellow" />
|
||||||
)}
|
)}
|
||||||
<p className="text-bunker-200 text-md font-base mt-0.5">{notification.text}</p>
|
<p className="text-md font-base mt-0.5 text-bunker-200">{notification.text}</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
onClick={() => clearNotification(notification.text)}
|
onClick={() => clearNotification(notification.text)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon className="absolute right-2 top-3 text-bunker-300 pl-2 w-4 h-4 hover:text-white" icon={faXmark} />
|
<FontAwesomeIcon
|
||||||
|
className="absolute right-2 top-3 h-4 w-4 pl-2 text-bunker-300 hover:text-white"
|
||||||
|
icon={faXmark}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -11,7 +11,7 @@ const Notifications = ({ notifications, clearNotification }: NoticationsProps) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
<div className="pointer-events-none fixed right-2 bottom-2 z-[100] hidden h-full w-96 gap-y-2 md:flex md:flex-col-reverse">
|
||||||
{notifications.map((notif) => (
|
{notifications.map((notif) => (
|
||||||
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
|
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
|
||||||
))}
|
))}
|
||||||
|
@@ -30,9 +30,9 @@ const ConfirmEnvOverwriteModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className='text-gray-400'>Your file contains the following duplicate secrets:</p>
|
<p className="text-gray-400">Your file contains the following duplicate secrets:</p>
|
||||||
<p className="text-sm text-gray-500">{duplicateKeys.join(", ")}</p>
|
<p className="text-sm text-gray-500">{duplicateKeys.join(", ")}</p>
|
||||||
<p className='text-md text-gray-400'>Are you sure you want to overwrite these secrets?</p>
|
<p className="text-md text-gray-400">Are you sure you want to overwrite these secrets?</p>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
import { memo, SyntheticEvent, useRef } from "react";
|
import { memo, SyntheticEvent, useRef } from "react";
|
||||||
import { faCircle, faCodeBranch, faExclamationCircle, faEye } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCircle,
|
||||||
|
faCodeBranch,
|
||||||
|
faExclamationCircle,
|
||||||
|
faEye
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import guidGenerator from "../utilities/randomId";
|
import guidGenerator from "../utilities/randomId";
|
||||||
@@ -61,28 +66,30 @@ const DashboardInputField = ({
|
|||||||
const error = startsWithNumber || isDuplicate;
|
const error = startsWithNumber || isDuplicate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex-col w-full h-10 ${
|
|
||||||
error && value !== "" ? "bg-red/[0.15]" : ""
|
|
||||||
} ${
|
|
||||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
|
||||||
}`}>
|
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center items-center h-full ${
|
className={`relative h-10 w-full flex-col ${error && value !== "" ? "bg-red/[0.15]" : ""} ${
|
||||||
|
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||||
error ? "w-max" : "w-full"
|
error ? "w-max" : "w-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)}
|
onChange={(e) =>
|
||||||
|
onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)
|
||||||
|
}
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
|
className={`ph-no-capture min-w-16 peer z-10 h-full w-full bg-transparent px-2 font-mono text-sm caret-bunker-200 outline-none ${
|
||||||
error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100"
|
error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100"
|
||||||
} duration-200`}
|
} duration-200`}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{startsWithNumber && (
|
{startsWithNumber && (
|
||||||
<div className='absolute right-2 top-2 text-red z-50'>
|
<div className="absolute right-2 top-2 z-50 text-red">
|
||||||
<HoverObject
|
<HoverObject
|
||||||
text="Secret names should not start with a number"
|
text="Secret names should not start with a number"
|
||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
@@ -91,7 +98,7 @@ const DashboardInputField = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isDuplicate && value !== "" && !startsWithNumber && (
|
{isDuplicate && value !== "" && !startsWithNumber && (
|
||||||
<div className='absolute right-2 top-2 text-red z-50'>
|
<div className="absolute right-2 top-2 z-50 text-red">
|
||||||
<HoverObject
|
<HoverObject
|
||||||
text="Secret names should be unique"
|
text="Secret names should be unique"
|
||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
@@ -99,10 +106,15 @@ const DashboardInputField = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
|
{!error && (
|
||||||
|
<div
|
||||||
|
className={`absolute right-0 top-0 z-50 bg-mineshaft-800 text-red group-hover:bg-mineshaft-700 ${
|
||||||
overrideEnabled ? "visible" : "invisible group-hover:visible"
|
overrideEnabled ? "visible" : "invisible group-hover:visible"
|
||||||
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
} duration-0 flex h-10 cursor-pointer items-center px-2`}
|
||||||
<button type="button" onClick={() => {
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
if (modifyValueOverride) {
|
if (modifyValueOverride) {
|
||||||
if (overrideEnabled === false) {
|
if (overrideEnabled === false) {
|
||||||
modifyValueOverride("", id);
|
modifyValueOverride("", id);
|
||||||
@@ -110,14 +122,20 @@ const DashboardInputField = ({
|
|||||||
modifyValueOverride(undefined, id);
|
modifyValueOverride(undefined, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<HoverObject
|
<HoverObject
|
||||||
text={overrideEnabled ? "This secret is overriden with your personal value" : "You can override this secret with a personal value"}
|
text={
|
||||||
|
overrideEnabled
|
||||||
|
? "This secret is overriden with your personal value"
|
||||||
|
: "You can override this secret with a personal value"
|
||||||
|
}
|
||||||
icon={faCodeBranch}
|
icon={faCodeBranch}
|
||||||
color={overrideEnabled ? "primary" : "bunker-400"}
|
color={overrideEnabled ? "primary" : "bunker-400"}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,20 +145,29 @@ const DashboardInputField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverObject text={value || ""} onChangeHandler={onChangeHandler} id={id}>
|
<PopoverObject text={value || ""} onChangeHandler={onChangeHandler} id={id}>
|
||||||
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
|
|
||||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
|
||||||
}`}>
|
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center items-center h-full ${
|
title={value}
|
||||||
|
className={`relative h-10 w-full flex-col overflow-hidden ${
|
||||||
|
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||||
error ? "w-max" : "w-full"
|
error ? "w-max" : "w-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value?.split("\n")[0] ? <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
|
{value?.split("\n")[0] ? (
|
||||||
|
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||||
{value?.split("\n")[0]}
|
{value?.split("\n")[0]}
|
||||||
</span> : <span className='text-bunker-400'>-</span> }
|
</span>
|
||||||
{value?.split("\n")[1] && <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
|
) : (
|
||||||
|
<span className="text-bunker-400">-</span>
|
||||||
|
)}
|
||||||
|
{value?.split("\n")[1] && (
|
||||||
|
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||||
{value?.split("\n")[1]}
|
{value?.split("\n")[1]}
|
||||||
</span>}
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverObject>
|
</PopoverObject>
|
||||||
@@ -148,10 +175,10 @@ const DashboardInputField = ({
|
|||||||
}
|
}
|
||||||
if (type === "value") {
|
if (type === "value") {
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="w-full flex-col">
|
||||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
|
<div className="group relative flex w-full flex-col justify-center whitespace-pre">
|
||||||
{overrideEnabled === true && (
|
{overrideEnabled === true && (
|
||||||
<div className="bg-primary-500 rounded-sm absolute top-[0.1rem] right-[0.1rem] z-0 w-min text-xxs px-1 text-black opacity-80">
|
<div className="absolute top-[0.1rem] right-[0.1rem] z-0 w-min rounded-sm bg-primary-500 px-1 text-xxs text-black opacity-80">
|
||||||
Override enabled
|
Override enabled
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -160,20 +187,20 @@ const DashboardInputField = ({
|
|||||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||||
onScroll={syncScroll}
|
onScroll={syncScroll}
|
||||||
className={`${
|
className={`${
|
||||||
blurred
|
blurred ? "text-transparent focus:text-transparent active:text-transparent" : ""
|
||||||
? "text-transparent focus:text-transparent active:text-transparent"
|
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none duration-200 no-scrollbar`}
|
||||||
: ""
|
|
||||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${
|
className={`${
|
||||||
blurred && !overrideEnabled
|
blurred && !overrideEnabled
|
||||||
? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200"
|
? "text-bunker-800 duration-200 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
|
||||||
: ""
|
: ""
|
||||||
} ${overrideEnabled ? "text-primary-300" : "text-gray-400"}
|
} ${overrideEnabled ? "text-primary-300" : "text-gray-400"}
|
||||||
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? "invisible" : "visible"} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
absolute z-0 flex flex-row whitespace-pre font-mono ${
|
||||||
|
blurred ? "invisible" : "visible"
|
||||||
|
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar mt-0.5 h-10 w-full overflow-x-scroll bg-transparent px-2 py-2 text-sm outline-none duration-100 no-scrollbar peer-focus:visible`}
|
||||||
>
|
>
|
||||||
{value?.split(REGEX).map((word) => {
|
{value?.split(REGEX).map((word) => {
|
||||||
if (word.match(REGEX) !== null) {
|
if (word.match(REGEX) !== null) {
|
||||||
@@ -203,20 +230,24 @@ const DashboardInputField = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{blurred && (
|
{blurred && (
|
||||||
<div className={`absolute flex flex-row justify-between items-center z-0 peer pr-2 ${
|
<div
|
||||||
|
className={`peer absolute z-0 flex flex-row items-center justify-between pr-2 ${
|
||||||
isSideBarOpen ? "bg-mineshaft-700 duration-200" : "bg-mineshaft-800"
|
isSideBarOpen ? "bg-mineshaft-700 duration-200" : "bg-mineshaft-800"
|
||||||
} peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip`}>
|
} h-10 w-full text-clip text-bunker-400 duration-100 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden`}
|
||||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
>
|
||||||
|
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||||
{value?.split("").map(() => (
|
{value?.split("").map(() => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
className="text-xxs mr-0.5"
|
className="mr-0.5 text-xxs"
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{value?.split("").length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
|
{value?.split("").length === 0 && <span className="text-bunker-400/80">EMPTY</span>}
|
||||||
|
</div>
|
||||||
|
<div className="invisible z-[100] cursor-default group-hover:visible">
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
</div>
|
</div>
|
||||||
<div className='invisible group-hover:visible cursor-default z-[100]'><FontAwesomeIcon icon={faEye} /></div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -8,32 +8,35 @@ import Button from "../basic/buttons/Button";
|
|||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
isPlain?: boolean;
|
isPlain?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${
|
<div
|
||||||
|
className={`${
|
||||||
!isPlain
|
!isPlain
|
||||||
? "bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2"
|
? "ml-2 h-[2.5rem] w-[4.5rem] rounded-md bg-[#9B3535] opacity-70 duration-200 hover:opacity-100"
|
||||||
: "cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center"}`}>
|
: "justfy-center mr-2 flex h-[2.35rem] w-[1.5rem] cursor-pointer items-center"
|
||||||
{isPlain
|
}`}
|
||||||
? <div
|
>
|
||||||
|
{isPlain ? (
|
||||||
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
className="invisible group-hover:visible"
|
className="invisible group-hover:visible"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
|
<FontAwesomeIcon
|
||||||
|
className="mt-0.5 pl-2 pr-6 text-lg text-bunker-300 hover:text-red"
|
||||||
|
icon={faXmark}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
: <Button
|
) : (
|
||||||
text={String(t("Delete"))}
|
<Button text={String(t("Delete"))} color="red" size="md" onButtonPressed={onSubmit} />
|
||||||
color="red"
|
)}
|
||||||
size="md"
|
|
||||||
onButtonPressed={onSubmit}
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@@ -18,7 +18,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
|
|||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
as="div"
|
as="div"
|
||||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||||
>
|
>
|
||||||
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -31,7 +31,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute z-[90] drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
<Menu.Items className="absolute right-0 z-[90] mt-0.5 w-[12rem] origin-top-right space-y-2 rounded-md border border-mineshaft-500 bg-bunker p-2 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Button
|
<Button
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
|
@@ -56,7 +56,7 @@ export default function NavHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center pt-6">
|
<div className="flex flex-row items-center pt-6">
|
||||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black min-w-[1.25rem]">
|
<div className="mr-2 flex h-5 w-5 min-w-[1.25rem] items-center justify-center rounded-md bg-primary text-sm text-black">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
||||||
|
@@ -6,7 +6,6 @@ import { useOrganization, useWorkspace } from "@app/context";
|
|||||||
|
|
||||||
import { Select, SelectItem, Tooltip } from "../v2";
|
import { Select, SelectItem, Tooltip } from "../v2";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the component at the top of almost every page.
|
* This is the component at the top of almost every page.
|
||||||
* It shows how to navigate to a certain page.
|
* It shows how to navigate to a certain page.
|
||||||
@@ -39,10 +38,12 @@ export default function NavHeaderSecrets({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${!isSnapshot && "absolute"} ml-6 flex flex-row items-center pt-6 cursor-default`}>
|
<div
|
||||||
|
className={`${!isSnapshot && "absolute"} ml-6 flex cursor-default flex-row items-center pt-6`}
|
||||||
|
>
|
||||||
<div className="mr-3 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
<div className="mr-3 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,20 +61,27 @@ export default function NavHeaderSecrets({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||||
{pageName === "Secrets"
|
{pageName === "Secrets" ? (
|
||||||
? <a className="text-md font-medium text-primary/80 hover:text-primary" href={`${router.asPath.split("?")[0]}`}>{pageName}</a>
|
<a
|
||||||
: <div className="text-md text-gray-400">{pageName}</div>}
|
className="text-md font-medium text-primary/80 hover:text-primary"
|
||||||
{currentEnv &&
|
href={`${router.asPath.split("?")[0]}`}
|
||||||
|
>
|
||||||
|
{pageName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="text-md text-gray-400">{pageName}</div>
|
||||||
|
)}
|
||||||
|
{currentEnv && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-sm text-gray-400" />
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-sm text-gray-400" />
|
||||||
<div className='pl-3 rounded-md hover:bg-bunker-100/10'>
|
<div className="rounded-md pl-3 hover:bg-bunker-100/10">
|
||||||
<Tooltip content="Select environment">
|
<Tooltip content="Select environment">
|
||||||
<Select
|
<Select
|
||||||
value={userAvailableEnvs?.filter(uae => uae.name === currentEnv)[0]?.slug}
|
value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value && onEnvChange) onEnvChange(value);
|
if (value && onEnvChange) onEnvChange(value);
|
||||||
}}
|
}}
|
||||||
className="text-md pl-0 font-medium text-primary/80 hover:text-primary bg-transparent"
|
className="text-md bg-transparent pl-0 font-medium text-primary/80 hover:text-primary"
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||||
>
|
>
|
||||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||||
@@ -84,7 +92,8 @@ export default function NavHeaderSecrets({
|
|||||||
</Select>
|
</Select>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,7 @@ import React, { useState } from "react";
|
|||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import { useSendVerificationEmail } from "@app/hooks/api";
|
||||||
useSendVerificationEmail
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import Error from "../basic/Error";
|
import Error from "../basic/Error";
|
||||||
import { Button } from "../v2";
|
import { Button } from "../v2";
|
||||||
@@ -90,8 +88,8 @@ export default function CodeInputStep({
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
||||||
<p className="text-md flex justify-center text-bunker-200">{t("signup.step2-message")}</p>
|
<p className="text-md flex justify-center text-bunker-200">{t("signup.step2-message")}</p>
|
||||||
<p className="text-md flex justify-center font-semibold my-1 text-bunker-200">{email} </p>
|
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">{email} </p>
|
||||||
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||||
<ReactCodeInput
|
<ReactCodeInput
|
||||||
name=""
|
name=""
|
||||||
inputMode="tel"
|
inputMode="tel"
|
||||||
@@ -102,7 +100,7 @@ export default function CodeInputStep({
|
|||||||
className="mt-6 mb-2"
|
className="mt-6 mb-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="block md:hidden w-max mt-4 mx-auto">
|
<div className="mx-auto mt-4 block w-max md:hidden">
|
||||||
<ReactCodeInput
|
<ReactCodeInput
|
||||||
name=""
|
name=""
|
||||||
inputMode="tel"
|
inputMode="tel"
|
||||||
@@ -114,26 +112,29 @@ export default function CodeInputStep({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{codeError && <Error text={t("signup.step2-code-error")} />}
|
{codeError && <Error text={t("signup.step2-code-error")} />}
|
||||||
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-l py-1 text-lg w-full">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={incrementStep}
|
onClick={incrementStep}
|
||||||
size="sm"
|
size="sm"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
className='h-14'
|
className="h-14"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
isLoading={isCodeInputCheckLoading}
|
isLoading={isCodeInputCheckLoading}
|
||||||
> {String(t("signup.verify"))} </Button>
|
>
|
||||||
|
{" "}
|
||||||
|
{String(t("signup.verify"))}{" "}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||||
<div className="mt-2 text-bunker-400 text-md flex flex-row">
|
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||||
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
||||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
{isResendingVerificationEmail
|
{isResendingVerificationEmail
|
||||||
? t("signup.step2-resend-progress")
|
? t("signup.step2-resend-progress")
|
||||||
: t("signup.step2-resend-submit")}
|
: t("signup.step2-resend-submit")}
|
||||||
@@ -141,7 +142,7 @@ export default function CodeInputStep({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-bunker-400 pb-2">{t("signup.step2-spam-alert")}</p>
|
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -57,19 +57,22 @@ export default function DonwloadBackupPDFStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
|
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
|
||||||
<p className="text-xl text-center font-medium flex flex-col justify-center items-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 mb-6 text-6xl text-bunker-200" />
|
<FontAwesomeIcon
|
||||||
|
icon={faWarning}
|
||||||
|
className="ml-2 mr-3 mb-6 pt-1 text-6xl text-bunker-200"
|
||||||
|
/>
|
||||||
{t("signup.step4-message")}
|
{t("signup.step4-message")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
|
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
||||||
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
|
<div className="m-2 mx-auto mt-4 flex w-full w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6 lg:w-1/6">
|
||||||
<span className="mb-2">
|
<span className="mb-2">
|
||||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
|
<div className="mx-auto mt-0 mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mt-4 md:mb-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
||||||
<div className="text-l py-1 text-lg w-full">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBackupKeyGenerate}
|
onClick={handleBackupKeyGenerate}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@@ -52,13 +52,13 @@ export default function EnterEmailStep({
|
|||||||
try {
|
try {
|
||||||
await mutateAsync({ email });
|
await mutateAsync({ email });
|
||||||
incrementStep();
|
incrementStep();
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
const { message = "Something went wrong" } = e.response?.data as { message: string};
|
const { message = "Something went wrong" } = e.response?.data as { message: string };
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: message
|
text: message
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,11 +66,11 @@ export default function EnterEmailStep({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="w-full md:px-6 mx-auto">
|
<div className="mx-auto w-full md:px-6">
|
||||||
<p className="text-xl font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
<p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-medium text-transparent">
|
||||||
{t("signup.step1-start")}
|
{t("signup.step1-start")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] m-auto rounded-lg mt-8">
|
<div className="m-auto mt-8 flex w-1/4 min-w-[20rem] flex-col items-center justify-center rounded-lg lg:w-1/6">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your email address..."
|
placeholder="Enter your email address..."
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
@@ -79,28 +79,35 @@ export default function EnterEmailStep({
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
className="h-12"
|
className="h-12"
|
||||||
/>
|
/>
|
||||||
{emailError && <p className="text-red-600 text-xs text-left w-full ml-1.5 mt-1.5">Please enter a valid email.</p>}
|
{emailError && (
|
||||||
|
<p className="ml-1.5 mt-1.5 w-full text-left text-xs text-red-600">
|
||||||
|
Please enter a valid email.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-1/6">
|
||||||
<div className="text-l py-1 text-lg w-full">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={emailCheck}
|
onClick={emailCheck}
|
||||||
size="sm"
|
size="sm"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
className='h-14'
|
className="h-14"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
> {String(t("signup.step1-submit"))} </Button>
|
>
|
||||||
|
{" "}
|
||||||
|
{String(t("signup.step1-submit"))}{" "}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mb-48 mt-2 flex w-full max-w-md flex-col items-center justify-center pt-2 md:mb-16 md:pb-2">
|
<div className="mx-auto mb-48 mt-2 flex w-full max-w-md flex-col items-center justify-center pt-2 md:mb-16 md:pb-2">
|
||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<button type="button" className="w-max pb-3 duration-200 hover:opacity-90">
|
<button type="button" className="w-max pb-3 duration-200 hover:opacity-90">
|
||||||
<span className="text-sm text-mineshaft-400 hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer">
|
<span className="cursor-pointer text-sm text-mineshaft-400 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
{t("signup.already-have-account")}
|
{t("signup.already-have-account")}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -40,55 +40,61 @@ export default function TeamInviteStep(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-max mx-auto min-w-lg h-full pb-4 px-8 mb-64 md:mb-32">
|
<div className="min-w-lg mx-auto mb-64 h-full w-max px-8 pb-4 md:mb-32">
|
||||||
<p className="text-2xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
<p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-2xl font-semibold text-transparent">
|
||||||
{t("signup.step5-invite-team")}
|
{t("signup.step5-invite-team")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center flex justify-center text-bunker-400 md:mx-8 mb-6 mt-4">
|
<p className="mb-6 mt-4 flex justify-center text-center text-bunker-400 md:mx-8">
|
||||||
{t("signup.step5-subtitle")}
|
{t("signup.step5-subtitle")}
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-mineshaft-800 border border-mineshaft-500 w-max mx-auto pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-6">
|
<div className="mx-auto mb-6 w-max rounded-xl border border-mineshaft-500 bg-mineshaft-800 px-8 pt-6 pb-4 drop-shadow-xl">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-bunker-300 font-medium pl-1 pb-1 text-sm">
|
<div className="pl-1 pb-1 text-sm font-medium text-bunker-300">
|
||||||
<span>Emails</span>
|
<span>Emails</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="bg-mineshaft-900/70 min-w-[30rem] h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
className="h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
|
||||||
value={emails}
|
value={emails}
|
||||||
onChange={(e) => setEmails(e.target.value)}
|
onChange={(e) => setEmails(e.target.value)}
|
||||||
placeholder="email@example.com, email2@example.com..."
|
placeholder="email@example.com, email2@example.com..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-end justify-end mt-0 md:mt-4 md:mb-2 w-full md:min-w-[30rem] mt-2 md:max-w-md mx-auto text-sm">
|
<div className="mx-auto mt-0 mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverDetails?.emailConfigured) {
|
if (serverDetails?.emailConfigured) {
|
||||||
inviteUsers({ emails })
|
inviteUsers({ emails });
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("setUpEmail");
|
handlePopUpOpen("setUpEmail");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
// isFullWidth
|
// isFullWidth
|
||||||
className='h-10'
|
className="h-10"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
> {t("signup.step5-send-invites") ?? ""} </Button>
|
>
|
||||||
|
{" "}
|
||||||
|
{t("signup.step5-send-invites") ?? ""}{" "}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<EmailServiceSetupModal
|
<EmailServiceSetupModal
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 min-w-[20rem] max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 min-w-[20rem] max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={redirectToHome}
|
onClick={redirectToHome}
|
||||||
size="sm"
|
size="sm"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
className='h-12'
|
className="h-12"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
> {t("signup.step5-skip") ?? "Skip"} </Button>
|
>
|
||||||
|
{" "}
|
||||||
|
{t("signup.step5-skip") ?? "Skip"}{" "}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -196,10 +196,8 @@ export default function UserInfoStep({
|
|||||||
|
|
||||||
const userOrgs = await fetchOrganizations();
|
const userOrgs = await fetchOrganizations();
|
||||||
|
|
||||||
const orgSlug = userOrgs[0]?.slug;
|
|
||||||
const orgId = userOrgs[0]?.id;
|
const orgId = userOrgs[0]?.id;
|
||||||
const project = await ProjectService.initProject({
|
const project = await ProjectService.initProject({
|
||||||
organizationSlug: orgSlug,
|
|
||||||
projectName: "Example Project"
|
projectName: "Example Project"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,17 +1,11 @@
|
|||||||
import {
|
import { getAuthToken, setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/reactQuery";
|
||||||
getAuthToken,
|
|
||||||
setAuthToken,
|
|
||||||
setMfaTempToken,
|
|
||||||
setSignupTempToken} from "@app/reactQuery";
|
|
||||||
|
|
||||||
|
|
||||||
export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token";
|
export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token";
|
||||||
|
|
||||||
// depreciated: go for apiRequest module in config/api
|
// depreciated: go for apiRequest module in config/api
|
||||||
export default class SecurityClient {
|
export default class SecurityClient {
|
||||||
|
|
||||||
static setProviderAuthToken(tokenStr: string) {
|
static setProviderAuthToken(tokenStr: string) {
|
||||||
localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || "")
|
localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
static getProviderAuthToken() {
|
static getProviderAuthToken() {
|
||||||
|
@@ -3,9 +3,7 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import {
|
import { changePassword, srp1 } from "@app/hooks/api/auth/queries";
|
||||||
changePassword,
|
|
||||||
srp1} from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
||||||
import { deriveArgonKey } from "./cryptography/crypto";
|
import { deriveArgonKey } from "./cryptography/crypto";
|
||||||
@@ -18,12 +16,13 @@ type Params = {
|
|||||||
email: string;
|
email: string;
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
||||||
let serverPublicKey; let salt;
|
let serverPublicKey;
|
||||||
|
let salt;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||||
@@ -95,7 +94,6 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
|||||||
console.error(err2);
|
console.error(err2);
|
||||||
reject(err2);
|
reject(err2);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -104,6 +102,6 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export default attemptChangePassword;
|
export default attemptChangePassword;
|
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
|
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||||
import KeyService from "@app/services/KeyService";
|
import KeyService from "@app/services/KeyService";
|
||||||
|
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||||
@@ -12,10 +12,10 @@ const client = new jsrp.client();
|
|||||||
|
|
||||||
interface IsMfaLoginSuccessful {
|
interface IsMfaLoginSuccessful {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
loginResponse:{
|
loginResponse: {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
JTWToken: string;
|
JTWToken: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,20 +33,22 @@ const attemptLoginMfa = async ({
|
|||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string,
|
providerAuthToken?: string;
|
||||||
mfaToken: string;
|
mfaToken: string;
|
||||||
}): Promise<IsMfaLoginSuccessful> => {
|
}): Promise<IsMfaLoginSuccessful> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client.init({
|
client.init(
|
||||||
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password
|
password
|
||||||
}, async () => {
|
},
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
const clientPublicKey = client.getPublicKey();
|
const clientPublicKey = client.getPublicKey();
|
||||||
const { salt } = await login1({
|
const { salt } = await login1({
|
||||||
email,
|
email,
|
||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
providerAuthToken,
|
providerAuthToken
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -91,7 +93,7 @@ const attemptLoginMfa = async ({
|
|||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
success: true,
|
success: true,
|
||||||
loginResponse:{
|
loginResponse: {
|
||||||
privateKey,
|
privateKey,
|
||||||
JTWToken: token
|
JTWToken: token
|
||||||
}
|
}
|
||||||
@@ -99,8 +101,9 @@ const attemptLoginMfa = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default attemptLoginMfa;
|
export default attemptLoginMfa;
|
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
|
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||||
import KeyService from "@app/services/KeyService";
|
import KeyService from "@app/services/KeyService";
|
||||||
|
|
||||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||||
@@ -25,20 +25,22 @@ const attemptLoginMfa = async ({
|
|||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string,
|
providerAuthToken?: string;
|
||||||
mfaToken: string;
|
mfaToken: string;
|
||||||
}): Promise<Boolean> => {
|
}): Promise<Boolean> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
client.init({
|
client.init(
|
||||||
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password
|
password
|
||||||
}, async () => {
|
},
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
const clientPublicKey = client.getPublicKey();
|
const clientPublicKey = client.getPublicKey();
|
||||||
const { salt } = await login1({
|
const { salt } = await login1({
|
||||||
email,
|
email,
|
||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
providerAuthToken,
|
providerAuthToken
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -85,8 +87,9 @@ const attemptLoginMfa = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default attemptLoginMfa;
|
export default attemptLoginMfa;
|
@@ -1,5 +1,11 @@
|
|||||||
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
|
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
|
||||||
import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes";
|
import {
|
||||||
|
escapeCharRegex,
|
||||||
|
letterCharRegex,
|
||||||
|
lowEntropyRegexes,
|
||||||
|
numAndSpecialCharRegex,
|
||||||
|
repeatedCharRegex
|
||||||
|
} from "./passwordRegexes";
|
||||||
|
|
||||||
interface PasswordCheckProps {
|
interface PasswordCheckProps {
|
||||||
password: string;
|
password: string;
|
||||||
@@ -29,40 +35,38 @@ const passwordCheck = async ({
|
|||||||
{
|
{
|
||||||
name: "tooShort",
|
name: "tooShort",
|
||||||
validator: (pwd: string) => pwd.length >= 14,
|
validator: (pwd: string) => pwd.length >= 14,
|
||||||
setError: setPasswordErrorTooShort,
|
setError: setPasswordErrorTooShort
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tooLong",
|
name: "tooLong",
|
||||||
validator: (pwd: string) => pwd.length < 101,
|
validator: (pwd: string) => pwd.length < 101,
|
||||||
setError: setPasswordErrorTooLong,
|
setError: setPasswordErrorTooLong
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "noLetterChar",
|
name: "noLetterChar",
|
||||||
validator: (pwd: string) => letterCharRegex.test(pwd),
|
validator: (pwd: string) => letterCharRegex.test(pwd),
|
||||||
setError: setPasswordErrorNoLetterChar,
|
setError: setPasswordErrorNoLetterChar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "noNumOrSpecialChar",
|
name: "noNumOrSpecialChar",
|
||||||
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
|
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
|
||||||
setError: setPasswordErrorNoNumOrSpecialChar,
|
setError: setPasswordErrorNoNumOrSpecialChar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "repeatedChar",
|
name: "repeatedChar",
|
||||||
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
|
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
|
||||||
setError: setPasswordErrorRepeatedChar,
|
setError: setPasswordErrorRepeatedChar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "escapeChar",
|
name: "escapeChar",
|
||||||
validator: (pwd: string) => !escapeCharRegex.test(pwd),
|
validator: (pwd: string) => !escapeCharRegex.test(pwd),
|
||||||
setError: setPasswordErrorEscapeChar,
|
setError: setPasswordErrorEscapeChar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lowEntropy",
|
name: "lowEntropy",
|
||||||
validator: (pwd: string) => (
|
validator: (pwd: string) => !lowEntropyRegexes.some((regex) => regex.test(pwd)),
|
||||||
!lowEntropyRegexes.some(regex => regex.test(pwd))
|
setError: setPasswordErrorLowEntropy
|
||||||
),
|
}
|
||||||
setError: setPasswordErrorLowEntropy,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const isBreached = await checkIsPasswordBreached(password);
|
const isBreached = await checkIsPasswordBreached(password);
|
||||||
@@ -81,7 +85,7 @@ const passwordCheck = async ({
|
|||||||
} else {
|
} else {
|
||||||
test.setError(false);
|
test.setError(false);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return errorCheck;
|
return errorCheck;
|
||||||
};
|
};
|
||||||
|
@@ -17,25 +17,25 @@ function bufferToHex(buffer: ArrayBuffer): string {
|
|||||||
return hexParts.join("");
|
return hexParts.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
|
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
|
||||||
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
|
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
|
||||||
// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
|
// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
|
||||||
// returns a hash table of 800-1000 results
|
// returns a hash table of 800-1000 results
|
||||||
// padding has been added to prevent MitM attacker determining which hash table was called by the response size
|
// padding has been added to prevent MitM attacker determining which hash table was called by the response size
|
||||||
// the last 35 chars of the password hash are compared client-side against the table
|
// the last 35 chars of the password hash are compared client-side against the table
|
||||||
// if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted
|
// if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted
|
||||||
// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
|
// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
|
||||||
// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
|
// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
|
||||||
|
|
||||||
// The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
|
// The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
|
||||||
// "When processing requests to establish and change memorized secrets, verifiers SHALL compare
|
// "When processing requests to establish and change memorized secrets, verifiers SHALL compare
|
||||||
// the prospective secrets against a list that contains values known to be commonly-used, expected,
|
// the prospective secrets against a list that contains values known to be commonly-used, expected,
|
||||||
// or compromised. For example, the list MAY include, but is not limited to:
|
// or compromised. For example, the list MAY include, but is not limited to:
|
||||||
// • Passwords obtained from previous breach corpuses.
|
// • Passwords obtained from previous breach corpuses.
|
||||||
// • Dictionary words.
|
// • Dictionary words.
|
||||||
// • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
|
// • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
|
||||||
// • Context-specific words, such as the name of the service, the username, and derivatives
|
// • Context-specific words, such as the name of the service, the username, and derivatives
|
||||||
// thereof."
|
// thereof."
|
||||||
|
|
||||||
export const checkIsPasswordBreached = async (password: string): Promise<boolean> => {
|
export const checkIsPasswordBreached = async (password: string): Promise<boolean> => {
|
||||||
const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com";
|
const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com";
|
||||||
@@ -66,8 +66,8 @@ export const checkIsPasswordBreached = async (password: string): Promise<boolean
|
|||||||
response = await axios.get(rangedHashTableUri, {
|
response = await axios.get(rangedHashTableUri, {
|
||||||
headers: {
|
headers: {
|
||||||
"Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
"Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
||||||
"Content-Type": "text/plain",
|
"Content-Type": "text/plain"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
@@ -78,7 +78,6 @@ export const checkIsPasswordBreached = async (password: string): Promise<boolean
|
|||||||
return isBreachedPassword;
|
return isBreachedPassword;
|
||||||
}
|
}
|
||||||
retryAttempt += 1;
|
retryAttempt += 1;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!axios.isAxiosError(err)) {
|
if (!axios.isAxiosError(err)) {
|
||||||
throw err;
|
throw err;
|
||||||
@@ -88,14 +87,15 @@ export const checkIsPasswordBreached = async (password: string): Promise<boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
`Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API`
|
`Received a non-200 response (${
|
||||||
|
response ? response.status : "unknown"
|
||||||
|
}) from the Pwnd Passwords API`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("An unexpected error has occurred:", err.message);
|
console.error("An unexpected error has occurred:", err.message);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
// Clear the UTF-8 encoded password from memory
|
// Clear the UTF-8 encoded password from memory
|
||||||
|
|
||||||
if (encodedPwd) {
|
if (encodedPwd) {
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
|
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
|
||||||
import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes";
|
import {
|
||||||
|
escapeCharRegex,
|
||||||
|
letterCharRegex,
|
||||||
|
lowEntropyRegexes,
|
||||||
|
numAndSpecialCharRegex,
|
||||||
|
repeatedCharRegex
|
||||||
|
} from "./passwordRegexes";
|
||||||
|
|
||||||
type Errors = {
|
type Errors = {
|
||||||
tooShort?: string;
|
tooShort?: string;
|
||||||
@@ -44,40 +50,38 @@ const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Prom
|
|||||||
{
|
{
|
||||||
name: "tooShort",
|
name: "tooShort",
|
||||||
validator: (pwd: string) => pwd.length >= 14,
|
validator: (pwd: string) => pwd.length >= 14,
|
||||||
errorText: "at least 14 characters",
|
errorText: "at least 14 characters"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tooLong",
|
name: "tooLong",
|
||||||
validator: (pwd: string) => pwd.length < 101,
|
validator: (pwd: string) => pwd.length < 101,
|
||||||
errorText: "at most 100 characters",
|
errorText: "at most 100 characters"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "noLetterChar",
|
name: "noLetterChar",
|
||||||
validator: (pwd: string) => letterCharRegex.test(pwd),
|
validator: (pwd: string) => letterCharRegex.test(pwd),
|
||||||
errorText: "at least 1 letter character",
|
errorText: "at least 1 letter character"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "noNumOrSpecialChar",
|
name: "noNumOrSpecialChar",
|
||||||
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
|
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
|
||||||
errorText: "at least 1 number or special character",
|
errorText: "at least 1 number or special character"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "repeatedChar",
|
name: "repeatedChar",
|
||||||
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
|
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
|
||||||
errorText: "at most 3 repeated, consecutive characters",
|
errorText: "at most 3 repeated, consecutive characters"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "escapeChar",
|
name: "escapeChar",
|
||||||
validator: (pwd: string) => !escapeCharRegex.test(pwd),
|
validator: (pwd: string) => !escapeCharRegex.test(pwd),
|
||||||
errorText: "No escape characters allowed.",
|
errorText: "No escape characters allowed."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lowEntropy",
|
name: "lowEntropy",
|
||||||
validator: (pwd: string) => (
|
validator: (pwd: string) => !lowEntropyRegexes.some((regex) => regex.test(pwd)),
|
||||||
!lowEntropyRegexes.some(regex => regex.test(pwd))
|
errorText: "Password contains personal info."
|
||||||
),
|
}
|
||||||
errorText: "Password contains personal info.",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const isBreached = await checkIsPasswordBreached(password);
|
const isBreached = await checkIsPasswordBreached(password);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// This regex covers letters (case insensitive) for the top 50 most spoken languages
|
// This regex covers letters (case insensitive) for the top 50 most spoken languages
|
||||||
/* eslint-disable no-misleading-character-class */
|
/* eslint-disable no-misleading-character-class */
|
||||||
export const letterCharRegex = /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u;
|
export const letterCharRegex =
|
||||||
|
/[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u;
|
||||||
|
|
||||||
// This regex covers digits, special characters, symbols, and emojis.
|
// This regex covers digits, special characters, symbols, and emojis.
|
||||||
export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u;
|
export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u;
|
||||||
@@ -32,5 +33,5 @@ export const lowEntropyRegexes = [
|
|||||||
/\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/,
|
/\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/,
|
||||||
|
|
||||||
// US social security number
|
// US social security number
|
||||||
/\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/,
|
/\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/
|
||||||
];
|
];
|
@@ -3,9 +3,7 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
import { issueBackupPrivateKey ,
|
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
|
||||||
srp1
|
|
||||||
} from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
import generateBackupPDF from "../generateBackupPDF";
|
import generateBackupPDF from "../generateBackupPDF";
|
||||||
import Aes256Gcm from "./aes-256-gcm";
|
import Aes256Gcm from "./aes-256-gcm";
|
||||||
@@ -97,7 +95,6 @@ const issueBackupKey = async ({
|
|||||||
generatedKey
|
generatedKey
|
||||||
});
|
});
|
||||||
setBackupKeyIssued(true);
|
setBackupKeyIssued(true);
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
setBackupKeyError(true);
|
setBackupKeyError(true);
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
/* eslint-disable vars-on-top */
|
/* eslint-disable vars-on-top */
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
/* eslint-disable func-names */
|
/* eslint-disable func-names */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { INTERCOMid as APPid } from "@app/components/utilities/config";
|
import { INTERCOMid as APPid } from "@app/components/utilities/config";
|
||||||
|
@@ -3,11 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
|
||||||
import {
|
import { boot as bootIntercom, load as loadIntercom, update as updateIntercom } from "./intercom";
|
||||||
boot as bootIntercom,
|
|
||||||
load as loadIntercom,
|
|
||||||
update as updateIntercom,
|
|
||||||
} from "./intercom";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const IntercomProvider = ({ children }: { children: any }) => {
|
export const IntercomProvider = ({ children }: { children: any }) => {
|
||||||
@@ -16,7 +12,11 @@ export const IntercomProvider = ({ children }: { children: any }) => {
|
|||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
loadIntercom();
|
loadIntercom();
|
||||||
bootIntercom({name: `${user?.firstName || ""} ${user?.lastName || ""}`, email: user?.email || "", created_at: Math.floor(((new Date(user?.createdAt))?.getTime() || 0) / 1000)});
|
bootIntercom({
|
||||||
|
name: `${user?.firstName || ""} ${user?.lastName || ""}`,
|
||||||
|
email: user?.email || "",
|
||||||
|
created_at: Math.floor((new Date(user?.createdAt)?.getTime() || 0) / 1000)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@@ -2,4 +2,4 @@ export const isValidHexColor = (hexColor: string) => {
|
|||||||
const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
||||||
|
|
||||||
return hexColorPattern.test(hexColor);
|
return hexColorPattern.test(hexColor);
|
||||||
}
|
};
|
||||||
|
@@ -17,10 +17,9 @@ export const saveTokenToLocalStorage = ({
|
|||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
privateKey,
|
privateKey
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (protectedKey) {
|
if (protectedKey) {
|
||||||
localStorage.removeItem("protectedKey");
|
localStorage.removeItem("protectedKey");
|
||||||
localStorage.setItem("protectedKey", protectedKey);
|
localStorage.setItem("protectedKey", protectedKey);
|
||||||
@@ -62,9 +61,7 @@ export const saveTokenToLocalStorage = ({
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
throw new Error(
|
throw new Error(`Unable to send the tokens in local storage:${err.message}`);
|
||||||
`Unable to send the tokens in local storage:${ err.message}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { PostHog } from 'posthog-js';
|
import { PostHog } from "posthog-js";
|
||||||
import { initPostHog } from '@app/components/analytics/posthog';
|
import { initPostHog } from "@app/components/analytics/posthog";
|
||||||
import { ENV } from '@app/components/utilities/config';
|
import { ENV } from "@app/components/utilities/config";
|
||||||
|
|
||||||
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
||||||
|
|
||||||
@@ -13,23 +13,23 @@ class Capturer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
capture(item: string) {
|
capture(item: string) {
|
||||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
|
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
|
||||||
try {
|
try {
|
||||||
this.api.capture(item);
|
this.api.capture(item);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PostHog', error);
|
console.error("PostHog", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identify(id: string, email?: string) {
|
identify(id: string, email?: string) {
|
||||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
|
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
|
||||||
try {
|
try {
|
||||||
this.api.identify(id, {
|
this.api.identify(id, {
|
||||||
email: email
|
email: email
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PostHog', error);
|
console.error("PostHog", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ type Story = StoryObj<typeof Accordion>;
|
|||||||
|
|
||||||
export const Basic: Story = {
|
export const Basic: Story = {
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex w-full justify-center">
|
||||||
<Accordion {...args}>
|
<Accordion {...args}>
|
||||||
<AccordionItem value="section-1">
|
<AccordionItem value="section-1">
|
||||||
<AccordionTrigger>Section 1</AccordionTrigger>
|
<AccordionTrigger>Section 1</AccordionTrigger>
|
||||||
|
@@ -8,7 +8,7 @@ export const AccordionItem = forwardRef<HTMLDivElement, AccordionPrimitive.Accor
|
|||||||
({ children, className, ...props }, forwardedRef) => (
|
({ children, className, ...props }, forwardedRef) => (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mt-px overflow-hidden first:mt-0 data-[state=open]:border-l data-[state=open]:border-primary transition-all border-transparent",
|
"mt-px overflow-hidden border-transparent transition-all first:mt-0 data-[state=open]:border-l data-[state=open]:border-primary",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -27,7 +27,7 @@ export const AccordionTrigger = forwardRef<
|
|||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"py-2 px-4 group data-[state=open]:text-primary h-11 hover:text-primary flex flex-1 outline-none items-center justify-between ",
|
"group flex h-11 flex-1 items-center justify-between py-2 px-4 outline-none hover:text-primary data-[state=open]:text-primary ",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -36,7 +36,7 @@ export const AccordionTrigger = forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faChevronDown}
|
icon={faChevronDown}
|
||||||
className="ease-[cubic-bezier(0.87,_0,_0.13,_1)] transition-transform duration-300 group-data-[state=open]:rotate-180 text-sm"
|
className="text-sm transition-transform duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)] group-data-[state=open]:rotate-180"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
@@ -51,13 +51,13 @@ export const AccordionContent = forwardRef<
|
|||||||
>(({ children, className, ...props }, forwardedRef) => (
|
>(({ children, className, ...props }, forwardedRef) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp overflow-hidden",
|
"overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<div className="text-sm py-2 px-4">{children}</div>
|
<div className="py-2 px-4 text-sm">{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@@ -1 +1 @@
|
|||||||
export { Accordion, AccordionContent, AccordionItem,AccordionTrigger } from "./Accordion";
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./Accordion";
|
||||||
|
@@ -10,7 +10,7 @@ export type CardTitleProps = {
|
|||||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600 break-words",
|
"mb-5 break-words border-b border-mineshaft-600 px-6 py-4 font-sans text-lg font-normal",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -30,7 +30,7 @@ export const Checkbox = ({
|
|||||||
<div className="flex items-center font-inter text-bunker-300">
|
<div className="flex items-center font-inter text-bunker-300">
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex items-center flex-shrink-0 justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
|
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
|
||||||
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||||
isChecked && "bg-primary hover:bg-primary",
|
isChecked && "bg-primary hover:bg-primary",
|
||||||
Boolean(children) && "mr-3",
|
Boolean(children) && "mr-3",
|
||||||
@@ -46,7 +46,7 @@ export const Checkbox = ({
|
|||||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
<label className="text-sm whitespace-nowrap truncate" htmlFor={id}>
|
<label className="truncate whitespace-nowrap text-sm" htmlFor={id}>
|
||||||
{children}
|
{children}
|
||||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
@@ -58,7 +58,7 @@ export const DeleteActionModal = ({
|
|||||||
title={title}
|
title={title}
|
||||||
subTitle={subTitle}
|
subTitle={subTitle}
|
||||||
footerContent={
|
footerContent={
|
||||||
<div className="flex items-center mx-2">
|
<div className="mx-2 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
@@ -91,7 +91,11 @@ export const DeleteActionModal = ({
|
|||||||
}
|
}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
>
|
>
|
||||||
<Input value={inputData} onChange={(e) => setInputData(e.target.value)} placeholder="Type to delete..." />
|
<Input
|
||||||
|
value={inputData}
|
||||||
|
onChange={(e) => setInputData(e.target.value)}
|
||||||
|
placeholder="Type to delete..."
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@@ -45,7 +45,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
|||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={twMerge(drawerContentVariation({ direction, className }))}
|
className={twMerge(drawerContentVariation({ direction, className }))}
|
||||||
>
|
>
|
||||||
<Card isRounded={false} className="h-full w-full dark">
|
<Card isRounded={false} className="dark h-full w-full">
|
||||||
{title && (
|
{title && (
|
||||||
<CardTitle subTitle={subTitle} className="px-4">
|
<CardTitle subTitle={subTitle} className="px-4">
|
||||||
{title}
|
{title}
|
||||||
|
@@ -1 +1 @@
|
|||||||
export { Drawer, DrawerClose,DrawerContent, DrawerTrigger } from "./Drawer";
|
export { Drawer, DrawerClose, DrawerContent, DrawerTrigger } from "./Drawer";
|
||||||
|
@@ -25,7 +25,7 @@ type Story = StoryObj<typeof DropdownMenuContent>;
|
|||||||
|
|
||||||
export const Basic: Story = {
|
export const Basic: Story = {
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex w-full justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconButton ariaLabel="add">
|
<IconButton ariaLabel="add">
|
||||||
@@ -43,7 +43,7 @@ export const Basic: Story = {
|
|||||||
|
|
||||||
export const Icons: Story = {
|
export const Icons: Story = {
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex w-full justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconButton ariaLabel="add">
|
<IconButton ariaLabel="add">
|
||||||
@@ -65,7 +65,7 @@ export const Icons: Story = {
|
|||||||
|
|
||||||
export const WithDivider: Story = {
|
export const WithDivider: Story = {
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex w-full justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconButton ariaLabel="add">
|
<IconButton ariaLabel="add">
|
||||||
@@ -86,7 +86,7 @@ export const WithDivider: Story = {
|
|||||||
|
|
||||||
export const Group: Story = {
|
export const Group: Story = {
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex w-full justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconButton ariaLabel="add">
|
<IconButton ariaLabel="add">
|
||||||
|
@@ -24,7 +24,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
|||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -48,7 +48,7 @@ export const DropdownSubMenuContent = forwardRef<HTMLDivElement, DropdownSubMenu
|
|||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -66,7 +66,7 @@ export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
|
|||||||
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
|
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge("text-xs text-bunker-400 px-4 pt-2 pb-1", className)}
|
className={twMerge("px-4 pt-2 pb-1 text-xs text-bunker-400", className)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,14 +91,14 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
|
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
{icon && iconPos === "left" && <span className="flex items-center mr-2">{icon}</span>}
|
{icon && iconPos === "left" && <span className="mr-2 flex items-center">{icon}</span>}
|
||||||
<span className="flex-grow text-left">{children}</span>
|
<span className="flex-grow text-left">{children}</span>
|
||||||
{icon && iconPos === "right" && <span className="flex items-center ml-2">{icon}</span>}
|
{icon && iconPos === "right" && <span className="ml-2 flex items-center">{icon}</span>}
|
||||||
</Item>
|
</Item>
|
||||||
</DropdownMenuPrimitive.Item>
|
</DropdownMenuPrimitive.Item>
|
||||||
);
|
);
|
||||||
@@ -124,14 +124,14 @@ export const DropdownSubMenuTrigger = <T extends ElementType = "button">({
|
|||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
|
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
{icon && iconPos === "left" && <span className="flex items-center mr-2">{icon}</span>}
|
{icon && iconPos === "left" && <span className="mr-2 flex items-center">{icon}</span>}
|
||||||
<span className="flex-grow text-left">{children}</span>
|
<span className="flex-grow text-left">{children}</span>
|
||||||
{icon && iconPos === "right" && <span className="flex items-center ml-2">{icon}</span>}
|
{icon && iconPos === "right" && <span className="ml-2 flex items-center">{icon}</span>}
|
||||||
</Item>
|
</Item>
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
);
|
);
|
||||||
@@ -143,7 +143,7 @@ export const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupPro
|
|||||||
({ ...props }, ref) => (
|
({ ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Group
|
<DropdownMenuPrimitive.Group
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge("text-xs py-2 pl-3", props.className)}
|
className={twMerge("py-2 pl-3 text-xs", props.className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -159,7 +159,7 @@ export const DropdownMenuSeparator = forwardRef<
|
|||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge("h-[1px] bg-gray-700 m-1", className)}
|
className={twMerge("m-1 h-[1px] bg-gray-700", className)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@@ -10,7 +10,8 @@ export const EmailServiceSetupModal = ({ isOpen, onOpenChange }: Props): JSX.Ele
|
|||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent title="Email service not configured">
|
<ModalContent title="Email service not configured">
|
||||||
<p className="mb-4 text-bunker-300">
|
<p className="mb-4 text-bunker-300">
|
||||||
The administrators of this Infisical instance have not yet set up an email service provider required to perform this action
|
The administrators of this Infisical instance have not yet set up an email service provider
|
||||||
|
required to perform this action
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="https://infisical.com/docs/self-hosting/configuration/email">
|
<a href="https://infisical.com/docs/self-hosting/configuration/email">
|
||||||
|
@@ -23,7 +23,7 @@ export const FormLabel = ({ id, label, isRequired, icon, className }: FormLabelP
|
|||||||
{label}
|
{label}
|
||||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||||
{icon && (
|
{icon && (
|
||||||
<span className="ml-2 text-mineshaft-300 hover:text-mineshaft-200 cursor-default">
|
<span className="ml-2 cursor-default text-mineshaft-300 hover:text-mineshaft-200">
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@@ -10,22 +10,16 @@ type Props = {
|
|||||||
|
|
||||||
export type HoverCardProps = Props;
|
export type HoverCardProps = Props;
|
||||||
|
|
||||||
export const HoverObject = ({
|
export const HoverObject = ({ text, icon, color }: Props): JSX.Element => (
|
||||||
text,
|
|
||||||
icon,
|
|
||||||
color
|
|
||||||
}: Props): JSX.Element => (
|
|
||||||
<HoverCard.Root openDelay={50}>
|
<HoverCard.Root openDelay={50}>
|
||||||
<HoverCard.Trigger asChild>
|
<HoverCard.Trigger asChild>
|
||||||
<a
|
<a className="ImageTrigger z-20">
|
||||||
className="ImageTrigger z-20"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} className={`text-${color}`} />
|
<FontAwesomeIcon icon={icon} className={`text-${color}`} />
|
||||||
</a>
|
</a>
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<HoverCard.Portal>
|
<HoverCard.Portal>
|
||||||
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
|
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
|
||||||
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
|
<div className="rounded-md border border-mineshaft-600 bg-bunker-700 p-2 text-bunker-300 drop-shadow-xl">
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 15 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 15 }}>
|
||||||
<div>
|
<div>
|
||||||
<div className="Text bold">{text}</div>
|
<div className="Text bold">{text}</div>
|
||||||
|
@@ -1 +1 @@
|
|||||||
export { HoverCard,HoverCardContent, HoverCardTrigger } from "./HoverCardv2";
|
export { HoverCard, HoverCardContent, HoverCardTrigger } from "./HoverCardv2";
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
|
@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
|||||||
<Card
|
<Card
|
||||||
isRounded
|
isRounded
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"fixed top-1/2 left-1/2 z-30 dark:[color-scheme:dark] max-h-screen thin-scrollbar max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
"thin-scrollbar fixed top-1/2 left-1/2 z-30 max-h-screen max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl dark:[color-scheme:dark]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -44,7 +44,7 @@ export const Pagination = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex items-center justify-end text-white w-full py-3 px-4 bg-mineshaft-800",
|
"flex w-full items-center justify-end bg-mineshaft-800 py-3 px-4 text-white",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -11,31 +11,34 @@ type Props = {
|
|||||||
|
|
||||||
export type PopoverProps = Props;
|
export type PopoverProps = Props;
|
||||||
|
|
||||||
export const PopoverObject = ({children, text, onChangeHandler, id}: Props) => (
|
export const PopoverObject = ({ children, text, onChangeHandler, id }: Props) => (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'>
|
<Popover.Trigger
|
||||||
|
asChild
|
||||||
|
className="data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
className="rounded z-[100] p-3 w-[460px] min-h-fit border border-chicago-700 bg-mineshaft-600 shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.violet7)] will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
className="z-[100] min-h-fit w-[460px] rounded border border-chicago-700 bg-mineshaft-600 p-3 shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] will-change-[transform,opacity] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.violet7)] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
hideWhenDetached
|
hideWhenDetached
|
||||||
side="left"
|
side="left"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col pt-2 dark">
|
<div className="dark flex flex-col pt-2">
|
||||||
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
|
<p className="mb-5 text-[15px] font-medium leading-[0px] text-bunker-200">Comment</p>
|
||||||
<textarea
|
<textarea
|
||||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||||
// type={type}
|
// type={type}
|
||||||
value={text}
|
value={text}
|
||||||
className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
|
className="ph-no-capture placeholder peer z-10 h-[20rem] w-full rounded-md border border-mineshaft-500 bg-bunker-600 py-2.5 px-2 text-sm text-bunker-300 caret-bunker-200 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent dark:[color-scheme:dark]"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder='–'
|
placeholder="–"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Popover.Close
|
<Popover.Close
|
||||||
className="rounded-full h-[25px] w-[25px] inline-flex items-center justify-center text-bunker-300 hover:text-white absolute top-[5px] right-[5px] hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 outline-none cursor-default"
|
className="hover:bg-violet4 focus:shadow-violet7 absolute top-[5px] right-[5px] inline-flex h-[25px] w-[25px] cursor-default items-center justify-center rounded-full text-bunker-300 outline-none hover:text-white focus:shadow-[0_0_0_2px]"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
@@ -1 +1 @@
|
|||||||
export { Popover,PopoverContent, PopoverTrigger } from "./Popoverv2";
|
export { Popover, PopoverContent, PopoverTrigger } from "./Popoverv2";
|
||||||
|
@@ -7,32 +7,32 @@ export type RadioGroupProps = RadioGroupPrimitive.RadioGroupProps;
|
|||||||
// Note this component is not customizable (Heroku integration and potentially other pages depend on it)
|
// Note this component is not customizable (Heroku integration and potentially other pages depend on it)
|
||||||
export const RadioGroup = ({ className, children, ...props }: RadioGroupProps) => (
|
export const RadioGroup = ({ className, children, ...props }: RadioGroupProps) => (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
className={twMerge("flex flex-row gap-5 px-6 mb-6", className)}
|
className={twMerge("mb-6 flex flex-row gap-5 px-6", className)}
|
||||||
defaultValue="App"
|
defaultValue="App"
|
||||||
aria-label="View density"
|
aria-label="View density"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
className="bg-bunker-400/20 w-[20px] h-[20px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
|
className="h-[20px] w-[20px] cursor-default rounded-full border border-bunker-400/60 bg-bunker-400/20 outline-none duration-200 hover:bg-bunker-400/40"
|
||||||
value="App"
|
value="App"
|
||||||
id="r1"
|
id="r1"
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[11px] after:h-[11px] after:rounded-[50%] after:bg-primary" />
|
<RadioGroupPrimitive.Indicator className="relative flex h-full w-full items-center justify-center after:block after:h-[11px] after:w-[11px] after:rounded-[50%] after:bg-primary after:content-['']" />
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r1">
|
<label className="pl-2 text-sm leading-none text-bunker-200" htmlFor="r1">
|
||||||
App
|
App
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
className="bg-bunker-400/20 w-[22px] h-[22px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
|
className="h-[22px] w-[22px] cursor-default rounded-full border border-bunker-400/60 bg-bunker-400/20 outline-none duration-200 hover:bg-bunker-400/40"
|
||||||
value="Pipeline"
|
value="Pipeline"
|
||||||
id="r2"
|
id="r2"
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[13px] after:h-[13px] after:rounded-[50%] after:bg-primary" />
|
<RadioGroupPrimitive.Indicator className="relative flex h-full w-full items-center justify-center after:block after:h-[13px] after:w-[13px] after:rounded-[50%] after:bg-primary after:content-['']" />
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r2">
|
<label className="pl-2 text-sm leading-none text-bunker-200" htmlFor="r2">
|
||||||
Pipeline
|
Pipeline
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -14,19 +14,20 @@ const replaceContentWithDot = (str: string) => {
|
|||||||
return finalStr;
|
return finalStr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
|
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
|
||||||
|
if (isImport) return "IMPORTED";
|
||||||
if (content === "") return "EMPTY";
|
if (content === "") return "EMPTY";
|
||||||
if (!content) return "EMPTY";
|
if (!content) return "EMPTY";
|
||||||
if (!isVisible) return replaceContentWithDot(content);
|
if (!isVisible) return replaceContentWithDot(content);
|
||||||
|
|
||||||
let skipNext = false;
|
let skipNext = false;
|
||||||
const formatedContent = content.split(REGEX).flatMap((el, i) => {
|
const formattedContent = content.split(REGEX).flatMap((el, i) => {
|
||||||
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
||||||
if (isInterpolationSyntax) {
|
if (isInterpolationSyntax) {
|
||||||
skipNext = true;
|
skipNext = true;
|
||||||
return (
|
return (
|
||||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
||||||
${<span className="ph-no-capture text-yello-200/80">{el.slice(2, -1)}</span>
|
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -40,12 +41,13 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
|
|||||||
|
|
||||||
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
||||||
// when break is added a line break works properly
|
// when break is added a line break works properly
|
||||||
return formatedContent.concat(<br />);
|
return formattedContent.concat(<br />);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
isImport?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
@@ -55,7 +57,17 @@ const commonClassName = "font-mono text-sm caret-white border-none outline-none
|
|||||||
|
|
||||||
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||||
(
|
(
|
||||||
{ value, isVisible, containerClassName, onBlur, isDisabled, isReadOnly, onFocus, ...props },
|
{
|
||||||
|
value,
|
||||||
|
isVisible,
|
||||||
|
isImport,
|
||||||
|
containerClassName,
|
||||||
|
onBlur,
|
||||||
|
isDisabled,
|
||||||
|
isReadOnly,
|
||||||
|
onFocus,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [isSecretFocused, setIsSecretFocused] = useToggle();
|
const [isSecretFocused, setIsSecretFocused] = useToggle();
|
||||||
@@ -69,7 +81,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
<pre aria-hidden className="m-0 ">
|
<pre aria-hidden className="m-0 ">
|
||||||
<code className={`inline-block w-full ${commonClassName}`}>
|
<code className={`inline-block w-full ${commonClassName}`}>
|
||||||
<span style={{ whiteSpace: "break-spaces" }}>
|
<span style={{ whiteSpace: "break-spaces" }}>
|
||||||
{syntaxHighlight(value, isVisible || isSecretFocused)}
|
{syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
|
||||||
</span>
|
</span>
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { forwardRef, ReactNode } from "react";
|
import { forwardRef, ReactNode } from "react";
|
||||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { faCaretDown, faCaretUp,faCheck } from "@fortawesome/free-solid-svg-icons";
|
import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`inline-flex items-center justify-between rounded-md
|
`inline-flex items-center justify-between rounded-md
|
||||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200`,
|
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -57,7 +57,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative top-1 z-[100] overflow-hidden rounded-md bg-mineshaft-900 border border-mineshaft-600 font-inter text-bunker-100 shadow-md",
|
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -106,7 +106,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative mb-0.5 flex
|
`relative mb-0.5 flex
|
||||||
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||||
outline-none transition-all hover:bg-mineshaft-500`,
|
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||||
isSelected && "bg-primary",
|
isSelected && "bg-primary",
|
||||||
isDisabled &&
|
isDisabled &&
|
||||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||||
|
@@ -20,7 +20,7 @@ export const Spinner = ({ className, size = "md" }: Props): JSX.Element => {
|
|||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"text-gray-200 animate-spin dark:text-gray-600 fill-primary m-1",
|
"m-1 animate-spin fill-primary text-gray-200 dark:text-gray-600",
|
||||||
sizeChart[size],
|
sizeChart[size],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
@@ -14,7 +14,7 @@ export const Stepper = ({ activeStep, children, direction, className }: StepperP
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex items-center w-full space-x-3 p-2 border border-bunker-300/30 rounded-md",
|
"flex w-full items-center space-x-3 rounded-md border border-bunker-300/30 p-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -25,15 +25,15 @@ export const Stepper = ({ activeStep, children, direction, className }: StepperP
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex items-center space-x-3 flex-shrink-0",
|
"flex flex-shrink-0 items-center space-x-3",
|
||||||
isNotLast && "flex-grow"
|
isNotLast && "flex-grow"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2 flex-shrink-0">
|
<div className="flex flex-shrink-0 items-center space-x-2">
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-7 h-7 flex items-center justify-center font-medium text-mineshaft-800 text-sm rounded-full transition-all",
|
"flex h-7 w-7 items-center justify-center rounded-full text-sm font-medium text-mineshaft-800 transition-all",
|
||||||
isCompleted ? "bg-primary" : "border text-bunker-300 border-primary/30",
|
isCompleted ? "bg-primary" : "border border-primary/30 text-bunker-300",
|
||||||
isActive && "bg-primary text-mineshaft-800"
|
isActive && "bg-primary text-mineshaft-800"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -71,7 +71,7 @@ export type StepProps = {
|
|||||||
export const Step = ({ title, description }: StepProps) => {
|
export const Step = ({ title, description }: StepProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col text-gray-300">
|
<div className="flex flex-col text-gray-300">
|
||||||
<div className="font-medium text-sm">{title}</div>
|
<div className="text-sm font-medium">{title}</div>
|
||||||
{description && <div className="text-xs">{description}</div>}
|
{description && <div className="text-xs">{description}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export type { StepperProps,StepProps } from "./Stepper";
|
export type { StepperProps, StepProps } from "./Stepper";
|
||||||
export { Step,Stepper } from "./Stepper";
|
export { Step, Stepper } from "./Stepper";
|
||||||
|
@@ -6,5 +6,6 @@ export type {
|
|||||||
TFootProps,
|
TFootProps,
|
||||||
THeadProps,
|
THeadProps,
|
||||||
ThProps,
|
ThProps,
|
||||||
TrProps} from "./Table";
|
TrProps
|
||||||
export { Table, TableContainer, TableSkeleton, TBody, Td, TFoot,Th, THead, Tr } from "./Table";
|
} from "./Table";
|
||||||
|
export { Table, TableContainer, TableSkeleton, TBody, Td, TFoot, Th, THead, Tr } from "./Table";
|
||||||
|
@@ -13,7 +13,7 @@ export type TabListProps = TabsPrimitive.TabsListProps;
|
|||||||
|
|
||||||
export const TabList = ({ className, children, ...props }: TabListProps) => (
|
export const TabList = ({ className, children, ...props }: TabListProps) => (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
className={twMerge("flex-shrink-0 flex border-b-2 border-mineshaft-800", className)}
|
className={twMerge("flex flex-shrink-0 border-b-2 border-mineshaft-800", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -25,7 +25,7 @@ export type TabProps = TabsPrimitive.TabsTriggerProps;
|
|||||||
export const Tab = ({ className, children, ...props }: TabProps) => (
|
export const Tab = ({ className, children, ...props }: TabProps) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"px-3 h-10 font-medium text-sm flex items-center justify-center select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 text-mineshaft-400 transition-all data-[state=active]:text-white data-[state=active]:border-b data-[state=active]:border-primary",
|
"flex h-10 select-none items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:border-primary data-[state=active]:text-white",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -38,7 +38,7 @@ export type TabPanelProps = TabsPrimitive.TabsContentProps;
|
|||||||
|
|
||||||
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
|
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
className={twMerge("outline-none flex-grow py-5 rounded-bl-md rounded-br-md", className)}
|
className={twMerge("flex-grow rounded-bl-md rounded-br-md py-5 outline-none", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export type { TabListProps,TabPanelProps, TabProps, TabsProps } from "./Tabs";
|
export type { TabListProps, TabPanelProps, TabProps, TabsProps } from "./Tabs";
|
||||||
export { Tab, TabList, TabPanel, Tabs } from "./Tabs";
|
export { Tab, TabList, TabPanel, Tabs } from "./Tabs";
|
||||||
|
@@ -11,6 +11,8 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
|
|||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
position?: "top" | "bottom" | "left" | "right";
|
position?: "top" | "bottom" | "left" | "right";
|
||||||
|
isDisabled?: boolean;
|
||||||
|
center?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip = ({
|
export const Tooltip = ({
|
||||||
@@ -20,7 +22,9 @@ export const Tooltip = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
defaultOpen,
|
defaultOpen,
|
||||||
className,
|
className,
|
||||||
|
center,
|
||||||
asChild = true,
|
asChild = true,
|
||||||
|
isDisabled,
|
||||||
position = "top",
|
position = "top",
|
||||||
...props
|
...props
|
||||||
}: TooltipProps) => (
|
}: TooltipProps) => (
|
||||||
@@ -38,11 +42,13 @@ export const Tooltip = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
||||||
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
||||||
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||||
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||||
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
|
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
|
||||||
`,
|
`,
|
||||||
|
isDisabled && "!hidden",
|
||||||
|
center && "text-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|