Merge branch 'main' into feat/ui-improvements

This commit is contained in:
mv-turtle
2023-02-13 17:33:23 -08:00
committed by GitHub
50 changed files with 1925 additions and 1246 deletions
+7 -3
View File
@@ -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
View File
@@ -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 -2
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -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);
}; };
+10 -1
View File
@@ -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
+219 -220
View File
@@ -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
}) })
} }
}) })
+86 -18
View File
@@ -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
}
+117 -86
View File
@@ -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 };
File diff suppressed because it is too large Load Diff
+27 -22
View File
@@ -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;
+25 -32
View File
@@ -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;
+15 -17
View File
@@ -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,
+58 -53
View File
@@ -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,
}; };
+8 -16
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
FROM alpine
RUN apk add --no-cache tini
COPY infisical /bin/infisical
ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"]
+2 -1
View File
@@ -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` |
+2 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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&apos;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>
+16 -2
View File
@@ -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';
+8 -4
View File
@@ -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]
+11 -17
View File
@@ -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>
+2 -2
View File
@@ -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;
+24 -12
View File
@@ -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}
+6
View File
@@ -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']);
+1 -1
View File
@@ -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">