mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 04:27:38 +00:00
Merge branch 'main' into feat/ui-improvements
This commit is contained in:
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
# run only against tags
|
# run only against tags
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -18,11 +18,16 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
- run: echo "Ref name ${{github.ref_name}}"
|
- run: echo "Ref name ${{github.ref_name}}"
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.19.3'
|
go-version: ">=1.19.3"
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: cli/go.sum
|
cache-dependency-path: cli/go.sum
|
||||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||||
@@ -49,4 +54,3 @@ jobs:
|
|||||||
run: sh cli/upload_to_cloudsmith.sh
|
run: sh cli/upload_to_cloudsmith.sh
|
||||||
env:
|
env:
|
||||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
|
|
||||||
|
|||||||
+33
-40
@@ -68,10 +68,10 @@ archives:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
replace_existing_draft: true
|
replace_existing_draft: true
|
||||||
mode: 'replace'
|
mode: "replace"
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ incpatch .Version }}-devel"
|
name_template: "{{ incpatch .Version }}-devel"
|
||||||
@@ -80,8 +80,8 @@ changelog:
|
|||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
- '^docs:'
|
- "^docs:"
|
||||||
- '^test:'
|
- "^test:"
|
||||||
|
|
||||||
# publishers:
|
# publishers:
|
||||||
# - name: fury.io
|
# - name: fury.io
|
||||||
@@ -109,30 +109,30 @@ brews:
|
|||||||
man1.install "manpages/infisical.1.gz"
|
man1.install "manpages/infisical.1.gz"
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: infisical
|
- id: infisical
|
||||||
package_name: infisical
|
package_name: infisical
|
||||||
builds:
|
builds:
|
||||||
- all-other-builds
|
- all-other-builds
|
||||||
vendor: Infisical, Inc
|
vendor: Infisical, Inc
|
||||||
homepage: https://infisical.com/
|
homepage: https://infisical.com/
|
||||||
maintainer: Infisical, Inc
|
maintainer: Infisical, Inc
|
||||||
description: The offical Infisical CLI
|
description: The offical Infisical CLI
|
||||||
license: MIT
|
license: MIT
|
||||||
formats:
|
formats:
|
||||||
- rpm
|
- rpm
|
||||||
- deb
|
- deb
|
||||||
- apk
|
- apk
|
||||||
- archlinux
|
- archlinux
|
||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
contents:
|
contents:
|
||||||
- src: ./completions/infisical.bash
|
- src: ./completions/infisical.bash
|
||||||
dst: /etc/bash_completion.d/infisical
|
dst: /etc/bash_completion.d/infisical
|
||||||
- src: ./completions/infisical.fish
|
- src: ./completions/infisical.fish
|
||||||
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
||||||
- src: ./completions/infisical.zsh
|
- src: ./completions/infisical.zsh
|
||||||
dst: /usr/share/zsh/site-functions/_infisical
|
dst: /usr/share/zsh/site-functions/_infisical
|
||||||
- src: ./manpages/infisical.1.gz
|
- src: ./manpages/infisical.1.gz
|
||||||
dst: /usr/share/man/man1/infisical.1.gz
|
dst: /usr/share/man/man1/infisical.1.gz
|
||||||
|
|
||||||
scoop:
|
scoop:
|
||||||
bucket:
|
bucket:
|
||||||
@@ -146,15 +146,14 @@ scoop:
|
|||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
aurs:
|
aurs:
|
||||||
-
|
- name: infisical-bin
|
||||||
name: infisical-bin
|
|
||||||
homepage: "https://infisical.com"
|
homepage: "https://infisical.com"
|
||||||
description: "The official Infisical CLI"
|
description: "The official Infisical CLI"
|
||||||
maintainers:
|
maintainers:
|
||||||
- Infisical, Inc <[email protected]>
|
- Infisical, Inc <[email protected]>
|
||||||
license: MIT
|
license: MIT
|
||||||
private_key: '{{ .Env.AUR_KEY }}'
|
private_key: "{{ .Env.AUR_KEY }}"
|
||||||
git_url: 'ssh://[email protected]/infisical-bin.git'
|
git_url: "ssh://[email protected]/infisical-bin.git"
|
||||||
package: |-
|
package: |-
|
||||||
# bin
|
# bin
|
||||||
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||||
@@ -169,19 +168,13 @@ aurs:
|
|||||||
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
||||||
# man pages
|
# man pages
|
||||||
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
||||||
|
|
||||||
# dockers:
|
# dockers:
|
||||||
# - dockerfile: goreleaser.dockerfile
|
# - dockerfile: cli/docker/Dockerfile
|
||||||
# goos: linux
|
# goos: linux
|
||||||
# goarch: amd64
|
# goarch: amd64
|
||||||
# ids:
|
# ids:
|
||||||
# - infisical
|
# - infisical
|
||||||
# image_templates:
|
# image_templates:
|
||||||
# - "infisical/cli:{{ .Version }}"
|
# - "infisical/cli:{{ .Version }}"
|
||||||
# - "infisical/cli:{{ .Major }}.{{ .Minor }}"
|
|
||||||
# - "infisical/cli:{{ .Major }}"
|
|
||||||
# - "infisical/cli:latest"
|
# - "infisical/cli:latest"
|
||||||
# build_flag_templates:
|
|
||||||
# - "--label=org.label-schema.schema-version=1.0"
|
|
||||||
# - "--label=org.label-schema.version={{.Version}}"
|
|
||||||
# - "--label=org.label-schema.name={{.ProjectName}}"
|
|
||||||
# - "--platform=linux/amd64"
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const PORT = process.env.PORT || 4000;
|
const PORT = process.env.PORT || 4000;
|
||||||
const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400');
|
const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400');
|
||||||
|
const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP
|
||||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
||||||
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
||||||
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||||
@@ -50,6 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!;
|
|||||||
export {
|
export {
|
||||||
PORT,
|
PORT,
|
||||||
EMAIL_TOKEN_LIFETIME,
|
EMAIL_TOKEN_LIFETIME,
|
||||||
|
INVITE_ONLY_SIGNUP,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
SALT_ROUNDS,
|
SALT_ROUNDS,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
|
|||||||
@@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIntegrationOptions = async (
|
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
||||||
req: Request,
|
return res.status(200).send({
|
||||||
res: Response
|
integrationOptions: INTEGRATION_OPTIONS,
|
||||||
) => {
|
});
|
||||||
return res.status(200).send({
|
};
|
||||||
integrationOptions: INTEGRATION_OPTIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
@@ -90,8 +87,8 @@ export const oAuthExchange = async (
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const saveIntegrationAccessToken = async (
|
export const saveIntegrationAccessToken = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
// TODO: refactor
|
// TODO: refactor
|
||||||
// TODO: check if access token is valid for each integration
|
// TODO: check if access token is valid for each integration
|
||||||
@@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
apps = await getApps({
|
apps = await getApps({
|
||||||
integrationAuth: req.integrationAuth,
|
integrationAuth: req.integrationAuth,
|
||||||
accessToken: req.accessToken
|
accessToken: req.accessToken,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get integration authorization applications'
|
message: "Failed to get integration authorization applications",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
try {
|
try {
|
||||||
integrationAuth = await revokeAccess({
|
integrationAuth = await revokeAccess({
|
||||||
integrationAuth: req.integrationAuth,
|
integrationAuth: req.integrationAuth,
|
||||||
accessToken: req.accessToken
|
accessToken: req.accessToken,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration authorization'
|
message: "Failed to delete integration authorization",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration
|
integration,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change environment or name of integration with id [integrationId]
|
* Change environment or name of integration with id [integrationId]
|
||||||
@@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
// TODO: add integration-specific validation to ensure that each
|
// TODO: add integration-specific validation to ensure that each
|
||||||
// integration has the correct fields populated in [Integration]
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
app,
|
app,
|
||||||
appId,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner, // github-specific integration param
|
owner, // github-specific integration param
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
integration = await Integration.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: req.integration._id
|
_id: req.integration._id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
app,
|
app,
|
||||||
appId,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner
|
owner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (integration) {
|
if (integration) {
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
EventService.handleEvent({
|
EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: integration.workspace.toString()
|
workspaceId: integration.workspace.toString(),
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to update integration'
|
message: "Failed to update integration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration
|
integration,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
try {
|
try {
|
||||||
const { integrationId } = req.params;
|
const { integrationId } = req.params;
|
||||||
|
|
||||||
integration = await Integration.findOneAndDelete({
|
integration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId
|
_id: integrationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) throw new Error('Failed to find integration');
|
if (!integration) throw new Error("Failed to find integration");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration'
|
message: "Failed to delete integration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration
|
integration,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -397,9 +397,21 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
|
|||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { organizationId } = req.params;
|
const { organizationId } = req.params;
|
||||||
const orgMemberships = await MembershipOrg.find({ organization: organizationId });
|
|
||||||
const userIds = orgMemberships.map(orgMembership => orgMembership.user);
|
const workspacesSet = (
|
||||||
const memberships = await Membership.find({ user: { $in: userIds } });
|
await Workspace.find(
|
||||||
|
{
|
||||||
|
organization: organizationId
|
||||||
|
},
|
||||||
|
'_id'
|
||||||
|
)
|
||||||
|
).map((w) => w._id.toString());
|
||||||
|
|
||||||
|
const memberships = (
|
||||||
|
await Membership.find({
|
||||||
|
workspace: { $in: workspacesSet }
|
||||||
|
}).populate('workspace')
|
||||||
|
);
|
||||||
const userToWorkspaceIds: any = {};
|
const userToWorkspaceIds: any = {};
|
||||||
|
|
||||||
memberships.forEach(membership => {
|
memberships.forEach(membership => {
|
||||||
@@ -411,15 +423,5 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIds = Object.values(userToWorkspaceIds).flat()
|
return res.json(userToWorkspaceIds);
|
||||||
const workspacesList = await Workspace.find({
|
|
||||||
organization: organizationId,
|
|
||||||
_id: { $in: workspaceIds }
|
|
||||||
});
|
|
||||||
|
|
||||||
const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds =>
|
|
||||||
_.map(workspaceIds, id => _.find(workspacesList, { _id: id }))
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json(populatedUserWorkspaces);
|
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config';
|
||||||
import { User, MembershipOrg } from '../../models';
|
import { User, MembershipOrg } from '../../models';
|
||||||
import { completeAccount } from '../../helpers/user';
|
import { completeAccount } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { issueTokens, createToken } from '../../helpers/auth';
|
import { issueTokens, createToken } from '../../helpers/auth';
|
||||||
import { INVITED, ACCEPTED } from '../../variables';
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||||
@@ -24,6 +25,14 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
email = req.body.email;
|
email = req.body.email;
|
||||||
|
|
||||||
|
if (INVITE_ONLY_SIGNUP) {
|
||||||
|
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||||
|
const userCount = await User.countDocuments({})
|
||||||
|
if (userCount != 0) {
|
||||||
|
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select('+publicKey');
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Membership,
|
Membership,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
IUser,
|
IUser,
|
||||||
ServiceToken,
|
ServiceToken,
|
||||||
ServiceTokenData
|
ServiceTokenData,
|
||||||
} from '../../models';
|
} from "../../models";
|
||||||
import {
|
import {
|
||||||
createWorkspace as create,
|
createWorkspace as create,
|
||||||
deleteWorkspace as deleteWork
|
deleteWorkspace as deleteWork,
|
||||||
} from '../../helpers/workspace';
|
} from "../../helpers/workspace";
|
||||||
import { addMemberships } from '../../helpers/membership';
|
import { addMemberships } from "../../helpers/membership";
|
||||||
import { ADMIN } from '../../variables';
|
import { ADMIN } from "../../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return public keys of members of workspace with id [workspaceId]
|
* Return public keys of members of workspace with id [workspaceId]
|
||||||
@@ -24,32 +24,31 @@ import { ADMIN } from '../../variables';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||||
let publicKeys;
|
let publicKeys;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
publicKeys = (
|
publicKeys = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).populate<{ user: IUser }>('user', 'publicKey')
|
}).populate<{ user: IUser }>("user", "publicKey")
|
||||||
)
|
).map((member) => {
|
||||||
.map((member) => {
|
return {
|
||||||
return {
|
publicKey: member.user.publicKey,
|
||||||
publicKey: member.user.publicKey,
|
userId: member.user._id,
|
||||||
userId: member.user._id
|
};
|
||||||
};
|
});
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.captureException(err);
|
||||||
Sentry.captureException(err);
|
return res.status(400).send({
|
||||||
return res.status(400).send({
|
message: "Failed to get workspace member public keys",
|
||||||
message: 'Failed to get workspace member public keys'
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
publicKeys
|
publicKeys,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
let users;
|
let users;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
users = await Membership.find({
|
users = await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).populate('user', '+publicKey');
|
}).populate("user", "+publicKey");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace members'
|
message: "Failed to get workspace members",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
users
|
users,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||||
let workspaces;
|
let workspaces;
|
||||||
try {
|
try {
|
||||||
workspaces = (
|
workspaces = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
user: req.user._id
|
user: req.user._id,
|
||||||
}).populate('workspace')
|
}).populate("workspace")
|
||||||
).map((m) => m.workspace);
|
).map((m) => m.workspace);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspaces'
|
message: "Failed to get workspaces",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspaces
|
workspaces,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspace = async (req: Request, res: Response) => {
|
export const getWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
workspace = await Workspace.findOne({
|
workspace = await Workspace.findOne({
|
||||||
_id: workspaceId
|
_id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace'
|
message: "Failed to get workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createWorkspace = async (req: Request, res: Response) => {
|
export const createWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceName, organizationId } = req.body;
|
const { workspaceName, organizationId } = req.body;
|
||||||
|
|
||||||
// validate organization membership
|
// validate organization membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId
|
organization: organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error("Failed to validate organization membership");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaceName.length < 1) {
|
if (workspaceName.length < 1) {
|
||||||
throw new Error('Workspace names must be at least 1-character long');
|
throw new Error("Workspace names must be at least 1-character long");
|
||||||
}
|
}
|
||||||
|
|
||||||
// create workspace and add user as member
|
// create workspace and add user as member
|
||||||
workspace = await create({
|
workspace = await create({
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
organizationId
|
organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await addMemberships({
|
await addMemberships({
|
||||||
userIds: [req.user._id],
|
userIds: [req.user._id],
|
||||||
workspaceId: workspace._id.toString(),
|
workspaceId: workspace._id.toString(),
|
||||||
roles: [ADMIN]
|
roles: [ADMIN],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to create workspace'
|
message: "Failed to create workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// delete workspace
|
// delete workspace
|
||||||
await deleteWork({
|
await deleteWork({
|
||||||
id: workspaceId
|
id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete workspace'
|
message: "Failed to delete workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted workspace'
|
message: "Successfully deleted workspace",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId
|
_id: workspaceId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name
|
name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to change workspace name'
|
message: "Failed to change workspace name",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed workspace name',
|
message: "Successfully changed workspace name",
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
let integrations;
|
let integrations;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
integrations = await Integration.find({
|
integrations = await Integration.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace integrations'
|
message: "Failed to get workspace integrations",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrations
|
integrations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,27 +280,27 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrationAuthorizations = async (
|
export const getWorkspaceIntegrationAuthorizations = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let authorizations;
|
let authorizations;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
authorizations = await IntegrationAuth.find({
|
authorizations = await IntegrationAuth.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace integration authorizations'
|
message: "Failed to get workspace integration authorizations",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
authorizations
|
authorizations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,26 +310,26 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceServiceTokens = async (
|
export const getWorkspaceServiceTokens = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokens;
|
let serviceTokens;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
// ?? FIX.
|
// ?? FIX.
|
||||||
serviceTokens = await ServiceToken.find({
|
serviceTokens = await ServiceToken.find({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace service tokens'
|
message: "Failed to get workspace service tokens",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokens
|
serviceTokens,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
|
|||||||
relatedWorkspace.environments.forEach(environment => {
|
relatedWorkspace.environments.forEach(environment => {
|
||||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
||||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
||||||
if (isReadBlocked) {
|
if (isReadBlocked && isWriteBlocked) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
accessibleEnvironments.push({
|
accessibleEnvironments.push({
|
||||||
name: environment.name,
|
name: environment.name,
|
||||||
slug: environment.slug,
|
slug: environment.slug,
|
||||||
isWriteDenied: isWriteBlocked
|
isWriteDenied: isWriteBlocked,
|
||||||
|
isReadDenied: isReadBlocked
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { EESecretService, EELogService } from '../../ee/services';
|
|||||||
import { postHogClient } from '../../services';
|
import { postHogClient } from '../../services';
|
||||||
import { getChannelFromUserAgent } from '../../utils/posthog';
|
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||||
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||||
@@ -298,27 +298,42 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
userEmail = req.serviceTokenData.user.email;
|
userEmail = req.serviceTokenData.user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// none service token case as service tokens are already scoped
|
// none service token case as service tokens are already scoped to env and project
|
||||||
|
let hasWriteOnlyAccess
|
||||||
if (!req.serviceTokenData) {
|
if (!req.serviceTokenData) {
|
||||||
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
|
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
|
||||||
if (!hasAccess) {
|
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
|
||||||
|
if (hasNoAccess) {
|
||||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let secrets: any
|
||||||
const [err, secrets] = await to(Secret.find(
|
if (hasWriteOnlyAccess) {
|
||||||
{
|
secrets = await Secret.find(
|
||||||
workspace: workspaceId,
|
{
|
||||||
environment,
|
workspace: workspaceId,
|
||||||
$or: [
|
environment,
|
||||||
{ user: userId },
|
$or: [
|
||||||
{ user: { $exists: false } }
|
{ user: userId },
|
||||||
],
|
{ user: { $exists: false } }
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
],
|
||||||
}
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
).populate("tags").then())
|
}
|
||||||
|
)
|
||||||
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
.select("secretKeyCiphertext secretKeyIV secretKeyTag")
|
||||||
|
} else {
|
||||||
|
secrets = await Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
).populate("tags")
|
||||||
|
}
|
||||||
|
|
||||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
|
|
||||||
@@ -356,6 +371,59 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getOnlySecretKeys = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment } = req.query;
|
||||||
|
|
||||||
|
let userId = "" // used for getting personal secrets for user
|
||||||
|
let userEmail = "" // used for posthog
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id;
|
||||||
|
userEmail = req.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
userEmail = req.serviceTokenData.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// none service token case as service tokens are already scoped
|
||||||
|
if (!req.serviceTokenData) {
|
||||||
|
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, secretKeys] = await to(Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
|
||||||
|
.then())
|
||||||
|
|
||||||
|
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||||
|
|
||||||
|
// readAction && await EELogService.createLog({
|
||||||
|
// userId: new Types.ObjectId(userId),
|
||||||
|
// workspaceId: new Types.ObjectId(workspaceId as string),
|
||||||
|
// actions: [readAction],
|
||||||
|
// channel,
|
||||||
|
// ipAddress: req.ip
|
||||||
|
// });
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secretKeys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update secret(s)
|
* Update secret(s)
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { Membership } from "../../models";
|
import { Membership } from "../../models";
|
||||||
|
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
|
||||||
|
|
||||||
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
|
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
|
||||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
@@ -16,3 +17,38 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||||
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
|
if (!membershipForWorkspace) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||||
|
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||||
|
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||||
|
|
||||||
|
// case: you have write only if read is blocked and write is not
|
||||||
|
if (isReadDisallowed && !isWriteDisallowed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||||
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
|
if (!membershipForWorkspace) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||||
|
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||||
|
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||||
|
|
||||||
|
if (isReadBlocked && isWriteDisallowed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from "@octokit/rest";
|
||||||
import { IIntegrationAuth } from '../models';
|
import { IIntegrationAuth } from "../models";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -12,12 +12,14 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL
|
INTEGRATION_FLYIO_API_URL,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names of apps for integration named [integration]
|
* Return list of names of apps for integration named [integration]
|
||||||
@@ -29,7 +31,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
const getApps = async ({
|
const getApps = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -54,40 +56,45 @@ const getApps = async ({
|
|||||||
break;
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
apps = await getAppsHeroku({
|
apps = await getAppsHeroku({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_VERCEL:
|
case INTEGRATION_VERCEL:
|
||||||
apps = await getAppsVercel({
|
apps = await getAppsVercel({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_NETLIFY:
|
case INTEGRATION_NETLIFY:
|
||||||
apps = await getAppsNetlify({
|
apps = await getAppsNetlify({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_GITHUB:
|
case INTEGRATION_GITHUB:
|
||||||
apps = await getAppsGithub({
|
apps = await getAppsGithub({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_RENDER:
|
case INTEGRATION_RENDER:
|
||||||
apps = await getAppsRender({
|
apps = await getAppsRender({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_FLYIO:
|
case INTEGRATION_FLYIO:
|
||||||
apps = await getAppsFlyio({
|
apps = await getAppsFlyio({
|
||||||
accessToken
|
accessToken,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_CIRCLECI:
|
||||||
|
apps = await getAppsCircleCI({
|
||||||
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get integration apps');
|
throw new Error("Failed to get integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
const res = (
|
const res = (
|
||||||
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
Accept: "application/vnd.heroku+json; version=3",
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
apps = res.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Heroku integration apps');
|
throw new Error("Failed to get Heroku integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -133,7 +140,7 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
*/
|
*/
|
||||||
const getAppsVercel = async ({
|
const getAppsVercel = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
|
|||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
...( integrationAuth?.teamId ? {
|
...(integrationAuth?.teamId
|
||||||
params: {
|
? {
|
||||||
teamId: integrationAuth.teamId
|
params: {
|
||||||
}
|
teamId: integrationAuth.teamId,
|
||||||
} : {})
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
apps = res.projects.map((a: any) => ({
|
apps = res.projects.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Vercel integration apps');
|
throw new Error("Failed to get Vercel integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
|
|||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsNetlify = async ({
|
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = (
|
const res = (
|
||||||
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
|
|||||||
|
|
||||||
apps = res.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.site_id
|
appId: a.site_id,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Netlify integration apps');
|
throw new Error("Failed to get Netlify integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
|
|||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsGithub = async ({
|
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: accessToken
|
auth: accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
const repos = (await octokit.request(
|
const repos = (
|
||||||
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
|
await octokit.request(
|
||||||
{
|
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
||||||
per_page: 100
|
{
|
||||||
}
|
per_page: 100,
|
||||||
)).data;
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
apps = repos
|
apps = repos
|
||||||
.filter((a:any) => a.permissions.admin === true)
|
.filter((a: any) => a.permissions.admin === true)
|
||||||
.map((a: any) => ({
|
.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
owner: a.owner.login
|
owner: a.owner.login,
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Github repos');
|
throw new Error("Failed to get Github repos");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
|
|||||||
* @returns {String} apps.name - name of Render service
|
* @returns {String} apps.name - name of Render service
|
||||||
* @returns {String} apps.appId - id of Render service
|
* @returns {String} apps.appId - id of Render service
|
||||||
*/
|
*/
|
||||||
const getAppsRender = async ({
|
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps: any;
|
let apps: any;
|
||||||
try {
|
try {
|
||||||
const res = (
|
const res = (
|
||||||
@@ -263,8 +261,8 @@ const getAppsRender = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
@@ -277,11 +275,11 @@ const getAppsRender = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Render services');
|
throw new Error("Failed to get Render services");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of apps for Fly.io integration
|
* Return list of apps for Fly.io integration
|
||||||
@@ -290,11 +288,7 @@ const getAppsRender = async ({
|
|||||||
* @returns {Object[]} apps - names and ids of Fly.io apps
|
* @returns {Object[]} apps - names and ids of Fly.io apps
|
||||||
* @returns {String} apps.name - name of Fly.io apps
|
* @returns {String} apps.name - name of Fly.io apps
|
||||||
*/
|
*/
|
||||||
const getAppsFlyio = async ({
|
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@@ -309,33 +303,70 @@ const getAppsFlyio = async ({
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const res = (await axios({
|
const res = (
|
||||||
url: INTEGRATION_FLYIO_API_URL,
|
await axios({
|
||||||
method: 'post',
|
url: INTEGRATION_FLYIO_API_URL,
|
||||||
headers: {
|
method: "post",
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
query,
|
query,
|
||||||
variables: {
|
variables: {
|
||||||
role: null
|
role: null,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})).data.data.apps.nodes;
|
})
|
||||||
|
).data.data.apps.nodes;
|
||||||
|
|
||||||
apps = res
|
apps = res.map((a: any) => ({
|
||||||
.map((a: any) => ({
|
name: a.name,
|
||||||
name: a.name
|
}));
|
||||||
}));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Fly.io apps');
|
throw new Error("Failed to get Fly.io apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of projects for CircleCI integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for CircleCI API
|
||||||
|
* @returns {Object[]} apps -
|
||||||
|
* @returns {String} apps.name - name of CircleCI apps
|
||||||
|
*/
|
||||||
|
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
let apps: any;
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await axios.get(
|
||||||
|
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data
|
||||||
|
|
||||||
|
apps = res?.map((a: any) => {
|
||||||
|
return {
|
||||||
|
name: a?.reponame
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error("Failed to get CircleCI projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
export { getApps };
|
export { getApps };
|
||||||
|
|||||||
+520
-413
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -31,44 +32,47 @@ export interface IIntegration {
|
|||||||
| 'netlify'
|
| 'netlify'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'render'
|
| 'render'
|
||||||
| 'flyio';
|
| 'flyio'
|
||||||
|
| 'circleci';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationSchema = new Schema<IIntegration>(
|
const integrationSchema = new Schema<IIntegration>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// name of app in provider
|
// name of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
appId: { // (new)
|
appId: {
|
||||||
|
// (new)
|
||||||
// id of app in provider
|
// id of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
targetEnvironment: { // (new)
|
targetEnvironment: {
|
||||||
|
// (new)
|
||||||
// target environment
|
// target environment
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
// github-specific repo owner-login
|
// github-specific repo owner-login
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
// aws-parameter-store-specific path
|
// aws-parameter-store-specific path
|
||||||
@@ -91,21 +95,22 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
],
|
],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
integrationAuth: {
|
integrationAuth: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'IntegrationAuth',
|
ref: "IntegrationAuth",
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Integration = model<IIntegration>('Integration', integrationSchema);
|
const Integration = model<IIntegration>("Integration", integrationSchema);
|
||||||
|
|
||||||
export default Integration;
|
export default Integration;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -8,24 +8,16 @@ import {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration:
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||||
| 'azure-key-vault'
|
teamId: string;
|
||||||
| 'aws-parameter-store'
|
accountId: string;
|
||||||
| 'aws-secret-manager'
|
|
||||||
| 'heroku'
|
|
||||||
| 'vercel'
|
|
||||||
| 'netlify'
|
|
||||||
| 'github'
|
|
||||||
| 'render'
|
|
||||||
| 'flyio';
|
|
||||||
teamId: string; // TODO: deprecate (vercel) -> move to accessId
|
|
||||||
accountId: string; // TODO: deprecate (netlify) -> move to accessId
|
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
refreshIV?: string;
|
refreshIV?: string;
|
||||||
refreshTag?: string;
|
refreshTag?: string;
|
||||||
@@ -41,9 +33,9 @@ export interface IIntegrationAuth {
|
|||||||
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
],
|
],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
teamId: {
|
teamId: {
|
||||||
// vercel-specific integration param
|
// vercel-specific integration param
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
accountId: {
|
accountId: {
|
||||||
// netlify-specific integration param
|
// netlify-specific integration param
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
refreshCiphertext: {
|
refreshCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
refreshIV: {
|
refreshIV: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
refreshTag: {
|
refreshTag: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessIdCiphertext: {
|
accessIdCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
},
|
},
|
||||||
accessCiphertext: {
|
accessCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessIV: {
|
accessIV: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessTag: {
|
accessTag: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessExpiresAt: {
|
accessExpiresAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
select: false
|
select: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const IntegrationAuth = model<IIntegrationAuth>(
|
const IntegrationAuth = model<IIntegrationAuth>(
|
||||||
'IntegrationAuth',
|
"IntegrationAuth",
|
||||||
integrationAuthSchema
|
integrationAuthSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import mongoose, { Schema, model } from 'mongoose';
|
||||||
|
import Secret, { ISecret } from './secret';
|
||||||
|
|
||||||
|
interface ISecretApprovalRequest {
|
||||||
|
secret: mongoose.Types.ObjectId;
|
||||||
|
requestedChanges: ISecret;
|
||||||
|
requestedBy: mongoose.Types.ObjectId;
|
||||||
|
approvers: IApprover[];
|
||||||
|
status: ApprovalStatus;
|
||||||
|
timestamp: Date;
|
||||||
|
requestType: RequestType;
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IApprover {
|
||||||
|
userId: mongoose.Types.ObjectId;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ApprovalStatus {
|
||||||
|
PENDING = 'pending',
|
||||||
|
APPROVED = 'approved',
|
||||||
|
REJECTED = 'rejected'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestType {
|
||||||
|
UPDATE = 'update',
|
||||||
|
DELETE = 'delete',
|
||||||
|
CREATE = 'create'
|
||||||
|
}
|
||||||
|
|
||||||
|
const approverSchema = new mongoose.Schema({
|
||||||
|
user: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: [ApprovalStatus],
|
||||||
|
default: ApprovalStatus.PENDING
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||||
|
{
|
||||||
|
secret: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Secret'
|
||||||
|
},
|
||||||
|
requestedChanges: Secret,
|
||||||
|
requestedBy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
approvers: [approverSchema],
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ApprovalStatus,
|
||||||
|
default: ApprovalStatus.PENDING
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
requestType: {
|
||||||
|
type: String,
|
||||||
|
enum: RequestType,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
requestId: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SecretApprovalRequest = model<ISecretApprovalRequest>('SecretApprovalRequest', secretApprovalRequestSchema);
|
||||||
|
|
||||||
|
export default SecretApprovalRequest;
|
||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
ENV_TESTING,
|
ENV_TESTING,
|
||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
ENV_SET
|
ENV_SET,
|
||||||
} from './environment';
|
} from "./environment";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -27,27 +28,22 @@ import {
|
|||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
} from './integration';
|
INTEGRATION_OPTIONS,
|
||||||
import {
|
} from "./integration";
|
||||||
OWNER,
|
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
||||||
ADMIN,
|
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
|
||||||
MEMBER,
|
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
} from './organization';
|
|
||||||
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
|
||||||
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
|
|
||||||
import {
|
import {
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
ACTION_LOGOUT,
|
ACTION_LOGOUT,
|
||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
ACTION_UPDATE_SECRETS,
|
ACTION_UPDATE_SECRETS,
|
||||||
ACTION_DELETE_SECRETS,
|
ACTION_DELETE_SECRETS,
|
||||||
ACTION_READ_SECRETS
|
ACTION_READ_SECRETS,
|
||||||
} from './action';
|
} from "./action";
|
||||||
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
|
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp";
|
||||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
import { PLAN_STARTER, PLAN_PRO } from "./stripe";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OWNER,
|
OWNER,
|
||||||
@@ -71,6 +67,7 @@ export {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -83,6 +80,7 @@ export {
|
|||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
|
|||||||
@@ -3,50 +3,53 @@ import {
|
|||||||
TENANT_ID_AZURE
|
TENANT_ID_AZURE
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
CLIENT_ID_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
CLIENT_ID_GITHUB,
|
CLIENT_ID_GITHUB,
|
||||||
CLIENT_SLUG_VERCEL
|
CLIENT_SLUG_VERCEL,
|
||||||
} from '../config';
|
} from "../config";
|
||||||
|
|
||||||
// integrations
|
// integrations
|
||||||
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||||
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
||||||
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
const INTEGRATION_HEROKU = "heroku";
|
||||||
const INTEGRATION_VERCEL = 'vercel';
|
const INTEGRATION_VERCEL = "vercel";
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
const INTEGRATION_NETLIFY = "netlify";
|
||||||
const INTEGRATION_GITHUB = 'github';
|
const INTEGRATION_GITHUB = "github";
|
||||||
const INTEGRATION_RENDER = 'render';
|
const INTEGRATION_RENDER = "render";
|
||||||
const INTEGRATION_FLYIO = 'flyio';
|
const INTEGRATION_FLYIO = "flyio";
|
||||||
|
const INTEGRATION_CIRCLECI = "circleci";
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
const INTEGRATION_OAUTH2 = "oauth2";
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||||
'https://api.vercel.com/v2/oauth/access_token';
|
"https://api.vercel.com/v2/oauth/access_token";
|
||||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
|
||||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
const INTEGRATION_GITHUB_TOKEN_URL =
|
||||||
'https://github.com/login/oauth/access_token';
|
"https://github.com/login/oauth/access_token";
|
||||||
|
|
||||||
// integration apps endpoints
|
// integration apps endpoints
|
||||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
|
||||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
||||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
||||||
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
|
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
||||||
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||||
|
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||||
|
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [
|
|||||||
clientId: '',
|
clientId: '',
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Circle CI',
|
||||||
|
slug: 'circleci',
|
||||||
|
image: 'Circle CI.png',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'pat',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Azure Key Vault',
|
name: 'Azure Key Vault',
|
||||||
slug: 'azure-key-vault',
|
slug: 'azure-key-vault',
|
||||||
@@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [
|
|||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Circle CI',
|
|
||||||
slug: 'circleci',
|
|
||||||
image: 'Circle CI.png',
|
|
||||||
isAvailable: false,
|
|
||||||
type: '',
|
|
||||||
clientId: '',
|
|
||||||
docsLink: ''
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -165,23 +168,25 @@ export {
|
|||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
INTEGRATION_AWS_SECRET_MANAGER,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
|
INTEGRATION_OPTIONS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
// membership roles
|
// membership roles
|
||||||
const OWNER = 'owner';
|
const OWNER = "owner";
|
||||||
const ADMIN = 'admin';
|
const ADMIN = "admin";
|
||||||
const MEMBER = 'member';
|
const MEMBER = "member";
|
||||||
|
|
||||||
// membership statuses
|
// membership statuses
|
||||||
const INVITED = 'invited';
|
const INVITED = "invited";
|
||||||
|
|
||||||
// membership permissions ability
|
// membership permissions ability
|
||||||
const ABILITY_READ = 'read';
|
const ABILITY_READ = "read";
|
||||||
const ABILITY_WRITE = 'write';
|
const ABILITY_WRITE = "write";
|
||||||
|
|
||||||
// -- organization
|
// -- organization
|
||||||
const ACCEPTED = 'accepted';
|
const ACCEPTED = "accepted";
|
||||||
|
|
||||||
export {
|
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
ABILITY_READ,
|
|
||||||
ABILITY_WRITE
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache tini
|
||||||
|
COPY infisical /bin/infisical
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"]
|
||||||
@@ -37,5 +37,6 @@ Configuring Infisical requires setting some environment variables. There is a fi
|
|||||||
| `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` |
|
| `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` |
|
||||||
| `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` |
|
| `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` |
|
||||||
| `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` |
|
| `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` |
|
||||||
| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` |
|
| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` |
|
||||||
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |
|
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |
|
||||||
|
| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` |
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
'netlify': 'Netlify',
|
'netlify': 'Netlify',
|
||||||
'github': 'GitHub',
|
'github': 'GitHub',
|
||||||
'render': 'Render',
|
'render': 'Render',
|
||||||
'flyio': 'Fly.io'
|
'flyio': 'Fly.io',
|
||||||
|
"circleci": 'CircleCI'
|
||||||
}
|
}
|
||||||
|
|
||||||
const envMapping: Mapping = {
|
const envMapping: Mapping = {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface Tag {
|
|||||||
export interface SecretDataProps {
|
export interface SecretDataProps {
|
||||||
pos: number;
|
pos: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string | undefined;
|
||||||
valueOverride: string | undefined;
|
valueOverride: string | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { plans } from 'public/data/frequentConstants';
|
import { plans } from 'public/data/frequentConstants';
|
||||||
|
|
||||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
@@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
ability: "read",
|
ability: "read",
|
||||||
environmentSlug: slug
|
environmentSlug: slug
|
||||||
}];
|
}];
|
||||||
|
} else if (val === "Add Only") {
|
||||||
|
denials = [{
|
||||||
|
ability: "read",
|
||||||
|
environmentSlug: slug
|
||||||
|
}];
|
||||||
} else {
|
} else {
|
||||||
denials = [];
|
denials = [];
|
||||||
}
|
}
|
||||||
@@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
||||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={isUpgradeModalOpen}
|
isOpen={isUpgradeModalOpen}
|
||||||
onClose={closeUpgradeModal}
|
onClose={closeUpgradeModal}
|
||||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||||
/>
|
/>
|
||||||
<table className="w-full my-0.5">
|
<table className="w-full my-0.5">
|
||||||
<thead className="text-gray-400 text-sm font-light">
|
<thead className="text-gray-400 text-xs font-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||||
{workspaceEnvs.map(env => (
|
{workspaceEnvs.map(env => (
|
||||||
<th key={guidGenerator()} className="text-left pl-8 py-1 max-w-min break-normal">
|
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
|
||||||
<span>{env.name.toUpperCase()}<br/></span>
|
<span>{env.slug.toUpperCase()}<br/></span>
|
||||||
{/* <span>PERMISSION</span> */}
|
{/* <span>PERMISSION</span> */}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
user.email?.toLowerCase().includes(filter)
|
user.email?.toLowerCase().includes(filter)
|
||||||
)
|
)
|
||||||
.map((row, index) => (
|
.map((row, index) => (
|
||||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
{row.firstName} {row.lastName}
|
{row.firstName} {row.lastName}
|
||||||
</td>
|
</td>
|
||||||
@@ -231,7 +236,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<div className="justify-start h-full flex flex-row items-center">
|
<div className="justify-start h-full flex flex-row items-center">
|
||||||
<Select
|
<Select
|
||||||
className="w-36"
|
className="w-36 bg-mineshaft-700"
|
||||||
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
// open={isOpen}
|
// open={isOpen}
|
||||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||||
value={row.role}
|
value={row.role}
|
||||||
@@ -253,23 +259,36 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-8 py-2 border-mineshaft-700 border-t text-gray-300">
|
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<Select
|
<Select
|
||||||
className="w-36"
|
className="w-16 bg-mineshaft-700"
|
||||||
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
|
position="item-aligned"
|
||||||
// open={isOpen}
|
// open={isOpen}
|
||||||
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
|
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
|
||||||
value={
|
value={
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
||||||
? "No Access"
|
? "No Access"
|
||||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") ? "Read Only" : "Read & Write")
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
|
||||||
|
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
||||||
|
? faEyeSlash
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
|
||||||
|
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
|
||||||
}
|
}
|
||||||
disabled={myRole !== 'admin'}
|
disabled={myRole !== 'admin'}
|
||||||
// onOpenChange={(open) => setIsOpen(open)}
|
// onOpenChange={(open) => setIsOpen(open)}
|
||||||
>
|
>
|
||||||
<SelectItem value="No Access">No Access</SelectItem>
|
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
|
||||||
<SelectItem value="Read Only">Read Only</SelectItem>
|
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
|
||||||
<SelectItem value="Read & Write">Read & Write</SelectItem>
|
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
|
||||||
|
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</td>)}
|
</td>)}
|
||||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
|
|||||||
|
|
||||||
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
|
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
|
||||||
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
||||||
|
import getOrganizationProjectMemberships from '@app/pages/api/organization/GetOrgProjectMemberships';
|
||||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
@@ -36,6 +37,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
);
|
);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [myRole, setMyRole] = useState('member');
|
const [myRole, setMyRole] = useState('member');
|
||||||
|
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
|
||||||
|
|
||||||
const workspaceId = router.query.id as string;
|
const workspaceId = router.query.id as string;
|
||||||
// Delete the row in the table (e.g. a user)
|
// Delete the row in the table (e.g. a user)
|
||||||
@@ -79,6 +81,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||||
|
(async () => {
|
||||||
|
const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))})
|
||||||
|
setUserProjectMemberships(result);
|
||||||
|
})();
|
||||||
}, [userData, myUser]);
|
}, [userData, myUser]);
|
||||||
|
|
||||||
const grantAccess = async (id: string, publicKey: string) => {
|
const grantAccess = async (id: string, publicKey: string) => {
|
||||||
@@ -110,7 +116,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max w-full">
|
||||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||||
<table className="w-full my-0.5">
|
<table className="w-full my-0.5">
|
||||||
<thead className="text-gray-400 text-sm font-light">
|
<thead className="text-gray-400 text-sm font-light">
|
||||||
@@ -118,6 +124,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||||
|
<th className="text-left pl-6 pr-10 py-3.5">PROJECTS</th>
|
||||||
<th aria-label="buttons" />
|
<th aria-label="buttons" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -189,6 +196,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
|
<div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
|
||||||
|
{userProjectMemberships[row.userId]
|
||||||
|
? userProjectMemberships[row.userId]?.map((project: any) => (
|
||||||
|
<div key={project._id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
|
||||||
|
<span className='mb-0.5 cursor-default'>{project.name}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn't part of any projects yet.</span>}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||||
{myUser !== row.email &&
|
{myUser !== row.email &&
|
||||||
// row.role !== "admin" &&
|
// row.role !== "admin" &&
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { faX } 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';
|
||||||
|
|
||||||
type NotificationType = 'success' | 'error' | 'info';
|
type NotificationType = 'success' | 'error' | 'info';
|
||||||
@@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative w-full flex items-center justify-between px-4 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
|
className="relative w-full flex items-center justify-between px-6 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{notification.type === 'error' && (
|
{notification.type === 'error' && (
|
||||||
@@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
|
|||||||
{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 w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
|
||||||
)}
|
)}
|
||||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p>
|
<p className="text-bunker-200 text-md font-base mt-0.5">{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="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} />
|
<FontAwesomeIcon className="absolute right-2 top-3 text-bunker-300 pl-2 w-4 h-4 hover:text-white" icon={faXmark} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { memo, SyntheticEvent, useRef } from 'react';
|
import { memo, SyntheticEvent, useRef } from 'react';
|
||||||
import { faCircle, faExclamationCircle, faEye, faLayerGroup } 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';
|
||||||
import { HoverObject } from '../v2/HoverCard';
|
import { HoverObject } from '../v2/HoverCard';
|
||||||
|
import { PopoverObject } from '../v2/Popover/Popover';
|
||||||
|
|
||||||
const REGEX = /([$]{.*?})/g;
|
const REGEX = /([$]{.*?})/g;
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ const DashboardInputField = ({
|
|||||||
}}>
|
}}>
|
||||||
<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={faLayerGroup}
|
icon={faCodeBranch}
|
||||||
color={overrideEnabled ? 'primary' : 'bunker-400'}
|
color={overrideEnabled ? 'primary' : 'bunker-400'}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -125,24 +126,24 @@ const DashboardInputField = ({
|
|||||||
const error = startsWithNumber || isDuplicate;
|
const error = startsWithNumber || isDuplicate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div title={value} className={`relative flex-col w-full h-10 ${
|
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}>
|
||||||
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
|
||||||
}`}>
|
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
||||||
<div
|
}`}>
|
||||||
className={`group relative flex flex-col justify-center items-center ${
|
<div
|
||||||
error ? 'w-max' : 'w-full'
|
className={`group relative flex flex-col justify-center items-center h-full ${
|
||||||
}`}
|
error ? 'w-max' : 'w-full'
|
||||||
>
|
}`}
|
||||||
<input
|
>
|
||||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
{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'>
|
||||||
type={type}
|
{value?.split("\n")[0]}
|
||||||
value={value}
|
</span> : <span className='text-bunker-400'>-</span> }
|
||||||
className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm 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")[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'>
|
||||||
spellCheck="false"
|
{value?.split("\n")[1]}
|
||||||
placeholder='–'
|
</span>}
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopoverObject>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (type === 'value') {
|
if (type === 'value') {
|
||||||
@@ -215,7 +216,7 @@ const DashboardInputField = ({
|
|||||||
))}
|
))}
|
||||||
{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>
|
||||||
<div className='invisible group-hover:visible cursor-pointer'><FontAwesomeIcon icon={faEye} /></div>
|
<div className='invisible group-hover:visible cursor-default z-[100]'><FontAwesomeIcon icon={faEye} /></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ const KeyPair = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/12 border-r border-mineshaft-600">
|
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
||||||
<div className="flex items-center max-h-16">
|
<div className="flex items-center max-h-16">
|
||||||
<DashboardInputField
|
<DashboardInputField
|
||||||
onChangeHandler={modifyComment}
|
onChangeHandler={modifyComment}
|
||||||
@@ -171,12 +171,26 @@ const KeyPair = ({
|
|||||||
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
|
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
|
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
|
||||||
<DeleteActionButton
|
{keyPair.key || keyPair.value
|
||||||
|
? <DeleteActionButton
|
||||||
onSubmit={() => { if (deleteRow) {
|
onSubmit={() => { if (deleteRow) {
|
||||||
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
|
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
|
||||||
}}}
|
}}}
|
||||||
isPlain
|
isPlain
|
||||||
/>
|
/>
|
||||||
|
: <div className='cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => { if (deleteRow) {
|
||||||
|
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
|
||||||
|
}}}
|
||||||
|
className="invisible group-hover:visible"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu';
|
|||||||
|
|
||||||
interface SecretProps {
|
interface SecretProps {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string | undefined;
|
||||||
valueOverride: string | undefined;
|
valueOverride: string | undefined;
|
||||||
pos: number;
|
pos: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -80,9 +80,9 @@ const SideBar = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
|
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-full min-w-sm max-w-sm sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loading.gif"
|
src="/images/loading/loading.gif"
|
||||||
height={60}
|
height={60}
|
||||||
@@ -91,7 +91,7 @@ const SideBar = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min overflow-y-auto">
|
<div className="h-min overflow-y-auto w-full">
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
|
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
|
||||||
<div
|
<div
|
||||||
@@ -186,7 +186,7 @@ const SideBar = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-full mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
|
<div className="mt-full w-96 mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
text="Compare secret across environments"
|
text="Compare secret across environments"
|
||||||
@@ -197,7 +197,7 @@ const SideBar = ({
|
|||||||
<CompareSecretsModal
|
<CompareSecretsModal
|
||||||
compareModal={compareModal}
|
compareModal={compareModal}
|
||||||
setCompareModal={setCompareModal}
|
setCompareModal={setCompareModal}
|
||||||
currentSecret={{ key: data[0]?.key, value: data[0]?.value }}
|
currentSecret={{ key: data[0]?.key, value: data[0]?.value ?? '' }}
|
||||||
workspaceEnvs={workspaceEnvs}
|
workspaceEnvs={workspaceEnvs}
|
||||||
selectedEnv={selectedEnv}
|
selectedEnv={selectedEnv}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ const CloudIntegration = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
cloudIntegrationOption.isAvailable
|
cloudIntegrationOption.isAvailable
|
||||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
? 'cursor-pointer duration-200 hover:bg-white/10'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
|
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!cloudIntegrationOption.isAvailable) return;
|
if (!cloudIntegrationOption.isAvailable) return;
|
||||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||||
@@ -61,22 +61,22 @@ const CloudIntegration = ({
|
|||||||
alt="integration logo"
|
alt="integration logo"
|
||||||
/>
|
/>
|
||||||
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
||||||
<div className="text-base">
|
<div className="text-base">
|
||||||
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
{cloudIntegrationOption.name}
|
{cloudIntegrationOption.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{cloudIntegrationOption.isAvailable &&
|
{cloudIntegrationOption.isAvailable &&
|
||||||
integrationAuths
|
integrationAuths
|
||||||
.map((authorization) => authorization.integration)
|
.map((authorization) => authorization?.integration)
|
||||||
.includes(cloudIntegrationOption.slug) && (
|
.includes(cloudIntegrationOption.slug) && (
|
||||||
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
<div className="group absolute top-0 right-0 z-40 flex flex-row">
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -86,8 +86,7 @@ const CloudIntegration = ({
|
|||||||
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||||
integrationAuthId: integrationAuths
|
integrationAuthId: integrationAuths
|
||||||
.filter(
|
.filter(
|
||||||
(authorization) =>
|
(authorization) => authorization.integration === cloudIntegrationOption.slug
|
||||||
authorization.integration === cloudIntegrationOption.slug
|
|
||||||
)
|
)
|
||||||
.map((authorization) => authorization._id)[0]
|
.map((authorization) => authorization._id)[0]
|
||||||
});
|
});
|
||||||
@@ -96,20 +95,20 @@ const CloudIntegration = ({
|
|||||||
integrationAuth: deletedIntegrationAuth
|
integrationAuth: deletedIntegrationAuth
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
|
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
|
||||||
Revoke
|
Revoke
|
||||||
</div>
|
</div>
|
||||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
|
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||||
Authorized
|
Authorized
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!cloudIntegrationOption.isAvailable && (
|
{!cloudIntegrationOption.isAvailable && (
|
||||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
<div className="group absolute top-0 right-0 z-50 flex flex-row">
|
||||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
|
||||||
Coming Soon
|
Coming Soon
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const IntegrationTile = ({
|
|||||||
|
|
||||||
// set initial environment. This find will only execute when component is mounting
|
// set initial environment. This find will only execute when component is mounting
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||||
environments.find(({ slug }) => slug === integration.environment) || {
|
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||||
name: '',
|
name: '',
|
||||||
slug: ''
|
slug: ''
|
||||||
}
|
}
|
||||||
@@ -69,9 +69,8 @@ const IntegrationTile = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadIntegration = async () => {
|
const loadIntegration = async () => {
|
||||||
|
|
||||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||||
integrationAuthId: integration.integrationAuth
|
integrationAuthId: integration?.integrationAuth
|
||||||
});
|
});
|
||||||
|
|
||||||
setApps(tempApps);
|
setApps(tempApps);
|
||||||
@@ -90,15 +89,16 @@ const IntegrationTile = ({
|
|||||||
case 'vercel':
|
case 'vercel':
|
||||||
setIntegrationTargetEnvironment(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.targetEnvironment
|
integration?.targetEnvironment
|
||||||
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
|
? integration.targetEnvironment.charAt(0).toUpperCase() +
|
||||||
: 'Development'
|
integration.targetEnvironment.substring(1)
|
||||||
|
: 'Development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'netlify':
|
case 'netlify':
|
||||||
setIntegrationTargetEnvironment(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.targetEnvironment
|
integration?.targetEnvironment
|
||||||
? contextNetlifyMapping[integration.targetEnvironment]
|
? contextNetlifyMapping[integration.targetEnvironment]
|
||||||
: 'Local development'
|
: 'Local development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -119,7 +119,7 @@ const IntegrationTile = ({
|
|||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
@@ -138,12 +138,12 @@ const IntegrationTile = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIntegrations(
|
setIntegrations(
|
||||||
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
|
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||||
@@ -152,7 +152,7 @@ const IntegrationTile = ({
|
|||||||
case 'vercel':
|
case 'vercel':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||||
isSelected={integrationTargetEnvironment}
|
isSelected={integrationTargetEnvironment}
|
||||||
@@ -164,7 +164,7 @@ const IntegrationTile = ({
|
|||||||
case 'netlify':
|
case 'netlify':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
|
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={
|
data={
|
||||||
!integration.isActive
|
!integration.isActive
|
||||||
@@ -189,10 +189,10 @@ const IntegrationTile = ({
|
|||||||
if (!integrationApp) return <div />;
|
if (!integrationApp) return <div />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
||||||
isSelected={integrationEnvironment.name}
|
isSelected={integrationEnvironment.name}
|
||||||
@@ -208,7 +208,7 @@ const IntegrationTile = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
|
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||||
@@ -218,7 +218,7 @@ const IntegrationTile = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
|
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||||
isSelected={integrationApp}
|
isSelected={integrationApp}
|
||||||
@@ -231,9 +231,9 @@ const IntegrationTile = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
{integration.isActive ? (
|
{integration.isActive ? (
|
||||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
|
||||||
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
|
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
|
||||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
<div className="font-semibold text-gray-300">In Sync</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -243,11 +243,13 @@ const IntegrationTile = ({
|
|||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handleDeleteIntegration({
|
onButtonPressed={() =>
|
||||||
integration
|
handleDeleteIntegration({
|
||||||
})}
|
integration
|
||||||
|
})
|
||||||
|
}
|
||||||
color="red"
|
color="red"
|
||||||
size="icon-md"
|
size="icon-md"
|
||||||
icon={faX}
|
icon={faX}
|
||||||
|
|||||||
@@ -35,16 +35,14 @@ const ProjectIntegrationSection = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
return integrations.length > 0 ? (
|
return integrations.length > 0 ? (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||||
<p className="text-base text-gray-400">
|
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
|
||||||
Manage integrations with third-party services.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{integrations.map((integration: Integration) => {
|
{integrations.map((integration: Integration) => {
|
||||||
return (
|
return (
|
||||||
<IntegrationTile
|
<IntegrationTile
|
||||||
key={`integration-${integration._id.toString()}`}
|
key={`integration-${integration?._id.toString()}`}
|
||||||
integration={integration}
|
integration={integration}
|
||||||
integrations={integrations}
|
integrations={integrations}
|
||||||
bot={bot}
|
bot={bot}
|
||||||
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
|
|||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProjectIntegrationSection;
|
export default ProjectIntegrationSection;
|
||||||
@@ -76,7 +76,7 @@ const encryptSecrets = async ({
|
|||||||
iv: secretValueIV,
|
iv: secretValueIV,
|
||||||
tag: secretValueTag
|
tag: secretValueTag
|
||||||
} = encryptSymmetric({
|
} = encryptSymmetric({
|
||||||
plaintext: secret.value,
|
plaintext: secret.value ?? '',
|
||||||
key: randomBytes
|
key: randomBytes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface EncryptedSecretProps {
|
|||||||
|
|
||||||
interface SecretProps {
|
interface SecretProps {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string | undefined;
|
||||||
type: 'personal' | 'shared';
|
type: 'personal' | 'shared';
|
||||||
comment: string;
|
comment: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -87,12 +87,17 @@ const getSecretsForProject = async ({
|
|||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const plainTextValue = decryptSymmetric({
|
let plainTextValue;
|
||||||
ciphertext: secret.secretValueCiphertext,
|
if (secret.secretValueCiphertext !== undefined) {
|
||||||
iv: secret.secretValueIV,
|
plainTextValue = decryptSymmetric({
|
||||||
tag: secret.secretValueTag,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
key
|
iv: secret.secretValueIV,
|
||||||
});
|
tag: secret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
plainTextValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let plainTextComment;
|
let plainTextComment;
|
||||||
if (secret.secretCommentCiphertext) {
|
if (secret.secretCommentCiphertext) {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: any;
|
||||||
|
text: string;
|
||||||
|
onChangeHandler: (value: string, position: number) => void;
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PopoverProps = Props;
|
||||||
|
|
||||||
|
export const PopoverObject = ({children, text, onChangeHandler, position}: Props) => (
|
||||||
|
<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'>
|
||||||
|
{children}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<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"
|
||||||
|
sideOffset={5}
|
||||||
|
hideWhenDetached
|
||||||
|
side="left"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col pt-2 dark">
|
||||||
|
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
|
||||||
|
<textarea
|
||||||
|
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||||
|
// type={type}
|
||||||
|
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'
|
||||||
|
spellCheck="false"
|
||||||
|
placeholder='–'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</Popover.Close>
|
||||||
|
<Popover.Arrow className="fill-chicago-700" />
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
PopoverObject.displayName = 'Popover';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type { PopoverProps } from './Popover';
|
||||||
|
export { PopoverObject } from './Popover';
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { forwardRef, ReactNode } from 'react';
|
import { forwardRef, ReactNode } from 'react';
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faChevronDown, faChevronUp } 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';
|
||||||
@@ -13,6 +14,7 @@ type Props = {
|
|||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: 'item-aligned' | 'popper';
|
position?: 'item-aligned' | 'popper';
|
||||||
|
icon?: IconProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
||||||
@@ -32,7 +34,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Value placeholder={placeholder} />
|
<SelectPrimitive.Value placeholder={placeholder}>
|
||||||
|
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||||
|
</SelectPrimitive.Value>
|
||||||
{!props.disabled && (
|
{!props.disabled && (
|
||||||
<SelectPrimitive.Icon className="ml-3">
|
<SelectPrimitive.Icon className="ml-3">
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||||
@@ -43,7 +47,6 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
||||||
|
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -77,6 +80,7 @@ Select.displayName = 'Select';
|
|||||||
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
|
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
customIcon?: IconProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||||
@@ -89,13 +93,13 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
||||||
outline-none transition-all hover:bg-mineshaft-500`,
|
outline-none transition-all hover:bg-mineshaft-500`,
|
||||||
isSelected && 'bg-primary',
|
isSelected && 'bg-primary',
|
||||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
|
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
|
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
|
||||||
<FontAwesomeIcon icon={faCheck} />
|
<FontAwesomeIcon icon={props.customIcon ? props.customIcon : faCheck} />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => {
|
|||||||
const value = useMemo<TOrgContext>(
|
const value = useMemo<TOrgContext>(
|
||||||
() => ({
|
() => ({
|
||||||
orgs: userOrgs,
|
orgs: userOrgs,
|
||||||
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
|
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
|
||||||
isLoading
|
isLoading
|
||||||
}),
|
}),
|
||||||
[currentWsOrgID, userOrgs, isLoading]
|
[currentWsOrgID, userOrgs, isLoading]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import getActionData from '@app/ee/api/secrets/GetActionData';
|
|||||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric
|
decryptSymmetric
|
||||||
@@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div
|
<div
|
||||||
className={`absolute border-l border-mineshaft-500 ${
|
className={`absolute border-l border-mineshaft-500 ${
|
||||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
} fixed h-[calc(100vh-56px)] w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
@@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min overflow-y-auto">
|
<div className="h-min">
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
<p className="font-semibold text-lg text-bunker-200">
|
<p className="font-semibold text-lg text-bunker-200">
|
||||||
{t(`activity:event.${actionMetaData?.name}`)}
|
{t(`activity:event.${actionMetaData?.name}`)}
|
||||||
@@ -157,7 +156,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col px-4">
|
<div className="flex flex-col px-4 overflow-y-auto h-[calc(100vh-120px)] overflow-y-autp">
|
||||||
{(actionMetaData?.name === 'readSecrets' ||
|
{(actionMetaData?.name === 'readSecrets' ||
|
||||||
actionMetaData?.name === 'addSecrets' ||
|
actionMetaData?.name === 'addSecrets' ||
|
||||||
actionMetaData?.name === 'deleteSecrets') &&
|
actionMetaData?.name === 'deleteSecrets') &&
|
||||||
@@ -166,14 +165,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
||||||
{item.newSecretVersion.key}
|
{item.newSecretVersion.key}
|
||||||
</div>
|
</div>
|
||||||
<DashboardInputField
|
<div className='w-full font-mono text-sm break-all bg-mineshaft-600 px-2 py-0.5 rounded-md border border-mineshaft-500 text-bunker-200'>
|
||||||
onChangeHandler={() => {}}
|
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
|
||||||
type="value"
|
</div>
|
||||||
position={1}
|
|
||||||
value={item.newSecretVersion.value}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{actionMetaData?.name === 'updateSecrets' &&
|
{actionMetaData?.name === 'updateSecrets' &&
|
||||||
@@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||||
{item.newSecretVersion.key}
|
{item.newSecretVersion.key}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
|
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
|
||||||
<div className="bg-red/30 px-2 ph-no-capture">
|
<div className="bg-red/40 px-2 ph-no-capture">
|
||||||
-{' '}
|
-{' '}
|
||||||
{patienceDiff(
|
{patienceDiff(
|
||||||
item.oldSecretVersion.value.split(''),
|
item.oldSecretVersion.value.split(''),
|
||||||
@@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
character.bIndex !== -1 && (
|
character.bIndex !== -1 && (
|
||||||
<span
|
<span
|
||||||
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
||||||
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
|
className={`${character.aIndex === -1 && 'text-bunker-100 bg-red-700/80'}`}
|
||||||
>
|
>
|
||||||
{character.line}
|
{character.line}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-500/30 px-2 ph-no-capture">
|
<div className="break-all bg-green-500/40 px-2 ph-no-capture">
|
||||||
+{' '}
|
+{' '}
|
||||||
{patienceDiff(
|
{patienceDiff(
|
||||||
item.oldSecretVersion.value.split(''),
|
item.oldSecretVersion.value.split(''),
|
||||||
@@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
character.aIndex !== -1 && (
|
character.aIndex !== -1 && (
|
||||||
<span
|
<span
|
||||||
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
||||||
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
|
className={`${character.bIndex === -1 && 'text-bunker-100 bg-green-700/80'}`}
|
||||||
>
|
>
|
||||||
{character.line}
|
{character.line}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -159,9 +159,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute border-l border-mineshaft-500 ${
|
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
|
||||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
} fixed h-full w-[28rem] right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
|
} fixed h-full right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
@@ -186,7 +186,8 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col px-2 py-2 overflow-y-auto h-[92vh]">
|
<div className="flex flex-col w-96 px-2 py-2 overflow-y-auto bg-bunker border-l border-mineshaft-600 h-[calc(100vh-115px)]">
|
||||||
|
<span className='px-2 text-bunker-200 pb-2 text-sm'>Note: This will recover secrets for all enviroments in this project.</span>
|
||||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
|||||||
}, [secretId]);
|
}, [secretId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
<div className="w-full min-w-40 h-[12.4rem] px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
||||||
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
||||||
<div className="p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
<div className="pl-1 py-0.5 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<Image
|
<Image
|
||||||
@@ -99,10 +99,10 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
|||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-0 h-full border-l mt-1" />
|
<div className="w-0 h-full border-l border-bunker-300 mt-1" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
||||||
<div className="pr-2 pt-1">
|
<div className="pr-2 pt-1 text-bunker-300/90">
|
||||||
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@@ -114,10 +114,10 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<p className="break-words ph-no-capture">
|
<p className="break-words ph-no-capture">
|
||||||
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
|
<span className="py-0.5 px-1 rounded-sm bg-primary-500/30 mr-1.5">
|
||||||
Value:
|
Value:
|
||||||
</span>
|
</span>
|
||||||
{version.value}
|
<span className='font-mono'>{version.value}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const putUserInWorkSpace = async () => {
|
const putUserInWorkSpace = async () => {
|
||||||
if (tempLocalStorage('orgData.id') === '') {
|
if (tempLocalStorage('orgData.id') === '') {
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
localStorage.setItem('orgData.id', userOrgs[0]._id);
|
localStorage.setItem('orgData.id', userOrgs[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgUserProjects = await getOrganizationUserProjects({
|
const orgUserProjects = await getOrganizationUserProjects({
|
||||||
@@ -123,7 +123,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||||
if (
|
if (
|
||||||
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
|
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
|
||||||
!userWorkspaces
|
!userWorkspaces
|
||||||
.map((workspace: { _id: string }) => workspace._id)
|
.map((workspace: { _id: string }) => workspace._id)
|
||||||
.includes(intendedWorkspaceId)
|
.includes(intendedWorkspaceId)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route lets us get all the project memebrships of users in an org.
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
const getOrganizationProjectMemberships = (req: { orgId: string }) =>
|
||||||
|
SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/workspace-memberships`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
console.log('Failed to get project memberships for users in an org');
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getOrganizationProjectMemberships;
|
||||||
@@ -53,12 +53,13 @@ type WorkspaceEnv = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
isWriteDenied: boolean;
|
isWriteDenied: boolean;
|
||||||
|
isReadDenied: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SecretDataProps {
|
interface SecretDataProps {
|
||||||
pos: number;
|
pos: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string | undefined;
|
||||||
valueOverride: string | undefined;
|
valueOverride: string | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
idOverride: string | undefined;
|
idOverride: string | undefined;
|
||||||
@@ -268,6 +269,8 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
setInitialData(dataToSort);
|
setInitialData(dataToSort);
|
||||||
reorderRows(dataToSort);
|
reorderRows(dataToSort);
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error', error);
|
console.log('Error', error);
|
||||||
@@ -318,7 +321,7 @@ export default function Dashboard() {
|
|||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
toggleSidebar('None');
|
toggleSidebar('None');
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
sortValuesHandler(
|
sortValuesHandler(
|
||||||
@@ -526,7 +529,7 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
|
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
|
||||||
}
|
}
|
||||||
if (selectedEnv && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
||||||
const secrets = await encryptSecrets({
|
const secrets = await encryptSecrets({
|
||||||
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
|
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -642,6 +645,11 @@ export default function Dashboard() {
|
|||||||
{new Date(snapshotData.createdAt).toLocaleString()}
|
{new Date(snapshotData.createdAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{selectedEnv?.isReadDenied && (
|
||||||
|
<span className="bg-primary-500 text-black text-sm ml-4 mt-1 px-1.5 rounded-md">
|
||||||
|
Add Only Mode
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && data?.length === 0 && selectedEnv && (
|
{!snapshotData && data?.length === 0 && selectedEnv && (
|
||||||
<ListBox
|
<ListBox
|
||||||
@@ -652,7 +660,8 @@ export default function Dashboard() {
|
|||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown',
|
slug: 'unknown',
|
||||||
isWriteDenied: false
|
isWriteDenied: false,
|
||||||
|
isReadDenied: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -661,7 +670,7 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="flex justify-start max-w-sm mt-1 mr-2">
|
<div className="flex justify-start max-w-sm mt-1 mr-2">
|
||||||
<Button
|
{!selectedEnv?.isReadDenied && <Button
|
||||||
text={String(`${numSnapshots} ${t('Commits')}`)}
|
text={String(`${numSnapshots} ${t('Commits')}`)}
|
||||||
onButtonPressed={() => {
|
onButtonPressed={() => {
|
||||||
toggleSidebar('None');
|
toggleSidebar('None');
|
||||||
@@ -670,7 +679,7 @@ export default function Dashboard() {
|
|||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
size="md"
|
size="md"
|
||||||
icon={faClockRotateLeft}
|
icon={faClockRotateLeft}
|
||||||
/>
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
||||||
<div className="flex justify-start max-w-sm mt-1">
|
<div className="flex justify-start max-w-sm mt-1">
|
||||||
@@ -738,7 +747,8 @@ export default function Dashboard() {
|
|||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown',
|
slug: 'unknown',
|
||||||
isWriteDenied: false
|
isWriteDenied: false,
|
||||||
|
isReadDenied: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -752,7 +762,8 @@ export default function Dashboard() {
|
|||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown',
|
slug: 'unknown',
|
||||||
isWriteDenied: false
|
isWriteDenied: false,
|
||||||
|
isReadDenied: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -770,7 +781,7 @@ export default function Dashboard() {
|
|||||||
placeholder={String(t('dashboard:search-keys'))}
|
placeholder={String(t('dashboard:search-keys'))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && (
|
{!snapshotData && !selectedEnv.isReadDenied && (
|
||||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||||
<DownloadSecretMenu data={data} env={selectedEnv.slug} />
|
<DownloadSecretMenu data={data} env={selectedEnv.slug} />
|
||||||
</div>
|
</div>
|
||||||
@@ -825,7 +836,7 @@ export default function Dashboard() {
|
|||||||
>
|
>
|
||||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||||
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
||||||
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
|
<div className='text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default'>0</div>
|
||||||
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
|
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
|
||||||
{!snapshotData && <IconButton
|
{!snapshotData && <IconButton
|
||||||
ariaLabel="copy icon"
|
ariaLabel="copy icon"
|
||||||
@@ -843,7 +854,7 @@ export default function Dashboard() {
|
|||||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
|
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/12 border-r border-mineshaft-600">
|
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
||||||
<div className="flex items-center max-h-16">
|
<div className="flex items-center max-h-16">
|
||||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
|
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -876,6 +887,7 @@ export default function Dashboard() {
|
|||||||
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|
||||||
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
|
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||||
.filter((row) => !sharedToHide.includes(row.id))
|
.filter((row) => !sharedToHide.includes(row.id))
|
||||||
|
.filter((row) => row.value !== undefined)
|
||||||
.map((keyPair) => (
|
.map((keyPair) => (
|
||||||
<KeyPair
|
<KeyPair
|
||||||
isCapitalized={autoCapitalization}
|
isCapitalized={autoCapitalization}
|
||||||
@@ -995,7 +1007,7 @@ export default function Dashboard() {
|
|||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
data={data.filter(
|
data={data.filter(
|
||||||
(row: SecretDataProps) =>
|
(row: SecretDataProps) =>
|
||||||
row.id === sidebarSecretId
|
row.id === sidebarSecretId && row.value !== undefined
|
||||||
)}
|
)}
|
||||||
modifyKey={listenChangeKey}
|
modifyKey={listenChangeKey}
|
||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ export default function Integrations() {
|
|||||||
case 'flyio':
|
case 'flyio':
|
||||||
link = `${window.location.origin}/integrations/flyio/authorize`
|
link = `${window.location.origin}/integrations/flyio/authorize`
|
||||||
break;
|
break;
|
||||||
|
case 'circleci':
|
||||||
|
link = `${window.location.origin}/integrations/circleci/authorize`
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -241,6 +244,9 @@ export default function Integrations() {
|
|||||||
case 'flyio':
|
case 'flyio':
|
||||||
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
||||||
break;
|
break;
|
||||||
|
case 'circleci':
|
||||||
|
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
} from '../../../components/v2';
|
||||||
|
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
|
||||||
|
|
||||||
|
export default function CircleCICreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [apiKey, setApiKey] = useState('');
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText('');
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText('API Key cannot be blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await saveIntegrationAccessToken({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
|
integration: 'circleci',
|
||||||
|
accessToken: apiKey,
|
||||||
|
accessId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Card className="max-w-md p-8 rounded-md">
|
||||||
|
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI API Key"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== '' ?? false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=''
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className='mt-4'
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to CircleCI
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCICreateIntegrationPage.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from '../../../components/v2';
|
||||||
|
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
|
||||||
|
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
|
||||||
|
import createIntegration from "../../api/integrations/createIntegration";
|
||||||
|
|
||||||
|
export default function CircleCICreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||||
|
const [targetApp, setTargetApp] = useState('');
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: handle case where apps can be empty
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
setTargetApp(integrationAuthApps[0]?.name);
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?._id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await createIntegration({
|
||||||
|
integrationAuthId: integrationAuth?._id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
targetEnvironment: null,
|
||||||
|
owner: null,
|
||||||
|
path: null,
|
||||||
|
region: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/integrations/${localStorage.getItem('projectData.id')}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Card className="max-w-md p-8 rounded-md">
|
||||||
|
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className='w-full border border-mineshaft-500'
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI Service"
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className='w-full border border-mineshaft-500'
|
||||||
|
>
|
||||||
|
{integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className='mt-4'
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCICreateIntegrationPage.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
|
||||||
@@ -159,7 +159,7 @@ export default function SettingsOrg() {
|
|||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
<div className="w-full max-h-screen pb-2">
|
||||||
<NavHeader pageName={t('settings-org:title')} />
|
<NavHeader pageName={t('settings-org:title')} />
|
||||||
<AddIncidentContactDialog
|
<AddIncidentContactDialog
|
||||||
isOpen={isAddIncidentContactOpen}
|
isOpen={isAddIncidentContactOpen}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function PersonalSettings() {
|
|||||||
setApiKeys={setApiKeys}
|
setApiKeys={setApiKeys}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
<div className="w-full max-h-screen pb-2">
|
||||||
<NavHeader pageName={t('settings-personal:title')} isProjectRelated={false} />
|
<NavHeader pageName={t('settings-personal:title')} isProjectRelated={false} />
|
||||||
<div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 text-xl max-w-5xl">
|
<div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 text-xl max-w-5xl">
|
||||||
<div className="flex flex-col justify-start items-start text-3xl">
|
<div className="flex flex-col justify-start items-start text-3xl">
|
||||||
|
|||||||
Reference in New Issue
Block a user