Compare commits

..

55 Commits

Author SHA1 Message Date
da857f321b allow to set default env in project file 2023-02-14 19:57:27 -08:00
c7dd028771 Update README.md 2023-02-14 18:34:41 -08:00
3c94bacda9 Update README.md 2023-02-14 18:31:45 -08:00
8e85847de3 improve example .env file format 2023-02-14 17:33:24 -08:00
0c10bbb569 send error message to standard error out 2023-02-14 13:43:40 -08:00
fba54ae0c6 Add tags query to secrets api 2023-02-13 22:28:59 -08:00
e243c72ca6 add tags flag to secrets related command 2023-02-13 22:28:30 -08:00
23ea6fd4f9 filter secrets by tags 2023-02-13 20:51:43 -08:00
3f9f2ef238 Merge pull request #329 from fervillarrealm/feature/52-save-changes-user-leaving-dashboard
feat(ui): save changes when user leaving dashboard
2023-02-13 19:47:10 -08:00
77cb20f5c7 Fixed a TS error 2023-02-13 19:44:18 -08:00
ddf630c269 Fixed a TS error 2023-02-13 19:00:43 -08:00
39adb9a0c2 Merge pull request #328 from akhilmhdh/feat/ui-improvements
feat(ui): add new button style, improved select ui and linted app layout
2023-02-13 17:44:41 -08:00
97fde96b7b Merge branch 'main' into feat/ui-improvements 2023-02-13 17:33:23 -08:00
190391e493 Fixed bugs with organizations and sidebars 2023-02-13 17:27:21 -08:00
6f6df3e63a Update approverSchema 2023-02-13 11:27:02 -08:00
23c740d225 setHasUnsavedChanges to false when user selects another env and they agree to not save changes 2023-02-13 11:07:22 -06:00
702d4de3b5 feature/52-save-changes-user-leaving-dashboard 2023-02-13 10:18:52 -06:00
445fa35ab5 Add Aashish to README 2023-02-13 18:55:09 +07:00
9868476965 Merge pull request #298 from Aashish-Upadhyay-101/circleci-integration-branch
Circleci integration branch
2023-02-13 18:03:22 +07:00
bfa6b955ca handleAuthorizedIntegrationOptionPress case circleci 2023-02-13 16:29:06 +05:45
90f5934440 projects displaying issue fixed using circleci v1.1 2023-02-13 15:32:37 +05:45
0adc3d2027 create secret approval data model 2023-02-12 23:20:27 -08:00
edf0294d51 Remove docker fine cli 2023-02-12 19:59:40 -08:00
8850b44115 Merge branch 'main' of https://github.com/Infisical/infisical 2023-02-12 17:54:55 -08:00
17f9e53779 Updated the dashabord, members, and settings pages 2023-02-12 17:54:22 -08:00
a61233d2ba Release docker images for cli 2023-02-12 14:22:59 -08:00
2022988e77 Only allow sign up when invted 2023-02-12 10:34:32 -08:00
409de81bd2 Allow sign up disable 2023-02-12 09:34:52 -08:00
2b289ddf77 feat(ui): add new button style, improved select ui and linted app layout 2023-02-12 17:31:22 +05:30
b066a55ead Show only secret keys if write only access 2023-02-11 23:41:51 -08:00
8dfc0138f5 circleci project name issue fixed 2023-02-12 09:41:34 +05:45
517f508e44 circleci Current Integrations section error fixed 2023-02-12 08:32:04 +05:45
2f1a671121 add workspace-memberships api 2023-02-11 15:16:33 -08:00
2fb4b261a8 Turn off auto delete and manual check ttl for token 2023-02-11 11:08:46 -08:00
9c3c745fdf small changes 2023-02-11 18:10:58 +05:45
6a75147719 circleci-done 2023-02-11 17:57:01 +05:45
295b363d8a Merge remote-tracking branch 'refs/remotes/origin/main' into circleci-integration-branch 2023-02-11 17:55:59 +05:45
d96b5943b9 circleci integration create.jsx and authorize.jsx created 2023-02-11 17:08:03 +05:45
8fd2578a6d fixed the bug with no projects 2023-02-10 23:16:37 -08:00
cc809a6bc0 Merge pull request #315 from akhilmhdh/feat/new-layout
Feat new layout synced with api changes
2023-02-10 22:20:11 -08:00
66659c8fc8 Bug/typo/style fixes and some minor improvements 2023-02-10 22:17:39 -08:00
31293bbe06 Remove tags from secrets when tag is deleted 2023-02-10 19:33:40 -08:00
1c3488f8db add reset infisical docs 2023-02-10 17:41:31 -08:00
20e536cec0 Remove printing pathToDir 2023-02-10 17:25:01 -08:00
e8b498ca6d Minor style tweaks 2023-02-10 16:45:31 -08:00
b5bcd0a308 feat(ui): updated merge conflicts in layout with new design 2023-02-10 22:09:22 +05:30
03c72ea00f feat(ui): added back layout change made for integrations page 2023-02-10 22:06:02 +05:30
a486390015 feat(ui): added new layout 2023-02-10 22:05:59 +05:30
8dc47110a0 feat(ui): added org context and user context 2023-02-10 22:03:57 +05:30
52a6fe64a7 feat(ui): new layout added queries 2023-02-10 22:03:57 +05:30
e6539a5566 Merge remote-tracking branch 'refs/remotes/origin/main' into circleci-integration-branch 2023-02-09 13:16:43 +05:45
07c056523f circle-ci integration done 2023-02-08 09:24:48 +05:45
80d219c3e0 circle-ci integration on progress 2023-02-07 13:20:39 +05:45
b0ffac2f00 fetch apps from circleci 2023-02-04 16:50:34 +05:45
5ba851adff circleci-integration-setup 2023-02-04 15:28:04 +05:45
110 changed files with 3366 additions and 1435 deletions

View File

@ -4,7 +4,7 @@ on:
push:
# run only against tags
tags:
- 'v*'
- "v*"
permissions:
contents: write
@ -18,11 +18,16 @@ jobs:
- uses: actions/checkout@v3
with:
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: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3
with:
go-version: '>=1.19.3'
go-version: ">=1.19.3"
cache: true
cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross
@ -45,8 +50,7 @@ jobs:
AUR_KEY: ${{ secrets.AUR_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith
- name: Publish to CloudSmith
run: sh cli/upload_to_cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

View File

@ -68,10 +68,10 @@ archives:
release:
replace_existing_draft: true
mode: 'replace'
mode: "replace"
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-devel"
@ -80,8 +80,8 @@ changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- "^docs:"
- "^test:"
# publishers:
# - name: fury.io
@ -109,30 +109,30 @@ brews:
man1.install "manpages/infisical.1.gz"
nfpms:
- id: infisical
package_name: infisical
builds:
- all-other-builds
vendor: Infisical, Inc
homepage: https://infisical.com/
maintainer: Infisical, Inc
description: The offical Infisical CLI
license: MIT
formats:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
contents:
- src: ./completions/infisical.bash
dst: /etc/bash_completion.d/infisical
- src: ./completions/infisical.fish
dst: /usr/share/fish/vendor_completions.d/infisical.fish
- src: ./completions/infisical.zsh
dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz
- id: infisical
package_name: infisical
builds:
- all-other-builds
vendor: Infisical, Inc
homepage: https://infisical.com/
maintainer: Infisical, Inc
description: The offical Infisical CLI
license: MIT
formats:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
contents:
- src: ./completions/infisical.bash
dst: /etc/bash_completion.d/infisical
- src: ./completions/infisical.fish
dst: /usr/share/fish/vendor_completions.d/infisical.fish
- src: ./completions/infisical.zsh
dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz
scoop:
bucket:
@ -146,15 +146,14 @@ scoop:
license: MIT
aurs:
-
name: infisical-bin
- name: infisical-bin
homepage: "https://infisical.com"
description: "The official Infisical CLI"
maintainers:
- Infisical, Inc <support@infisical.com>
license: MIT
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git"
package: |-
# bin
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"
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
# dockers:
# - dockerfile: goreleaser.dockerfile
# - dockerfile: cli/docker/Dockerfile
# goos: linux
# goarch: amd64
# ids:
# - infisical
# image_templates:
# - "infisical/cli:{{ .Version }}"
# - "infisical/cli:{{ .Major }}.{{ .Minor }}"
# - "infisical/cli:{{ .Major }}"
# - "infisical/cli:{{ .Version }}"
# - "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"

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
const PORT = process.env.PORT || 4000;
const EMAIL_TOKEN_LIFETIME = 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 SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
@ -24,7 +25,7 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY =
process.env.POSTHOG_PROJECT_API_KEY! ||
@ -50,6 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!;
export {
PORT,
EMAIL_TOKEN_LIFETIME,
INVITE_ONLY_SIGNUP,
ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME,

View File

@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
});
}
export const getIntegrationOptions = async (
req: Request,
res: Response
) => {
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS
});
}
export const getIntegrationOptions = async (req: Request, res: Response) => {
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
});
};
/**
* 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
*/
export const saveIntegrationAccessToken = async (
req: Request,
res: Response
req: Request,
res: Response
) => {
// TODO: refactor
// TODO: check if access token is valid for each integration
@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps;
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization applications'
});
}
let apps;
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({
apps
});
return res.status(200).send({
apps,
});
};
/**
@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration authorization'
});
}
return res.status(200).send({
integrationAuth
});
}
let integrationAuth;
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({
integrationAuth,
});
};

View File

@ -12,9 +12,9 @@ import { eventPushSecrets } from '../../events';
/**
* Create/initialize an (empty) integration for integration authorization
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
});
}
return res.status(200).send({
integration
});
}
return res.status(200).send({
integration,
});
};
/**
* Change environment or name of integration with id [integrationId]
@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner
},
{
new: true
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update integration'
});
}
let integration;
return res.status(200).send({
integration
});
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
}),
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
});
}
return res.status(200).send({
integration,
});
};
/**
@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let integration;
try {
const { integrationId } = req.params;
let integration;
try {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!integration) throw new Error('Failed to find integration');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration'
});
}
return res.status(200).send({
integration
});
integration = await Integration.findOneAndDelete({
_id: integrationId,
});
if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
return res.status(200).send({
integration,
});
};

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, EMAIL_TOKEN_LIFETIME } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../../helpers/signup';
@ -113,14 +113,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
invitee = await User.findOne({
email: inviteeEmail
}).select('+publicKey');
if (invitee) {
// case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
@ -170,7 +170,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
{
email: inviteeEmail,
token,
createdAt: new Date()
createdAt: new Date(),
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
},
{ upsert: true, new: true }
);
@ -241,7 +242,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
message: 'Successfully verified email',
user,
});
}
}
if (!user) {
// initialize user account

View File

@ -14,11 +14,13 @@ import {
MembershipOrg,
Organization,
Workspace,
IncidentContactOrg
IncidentContactOrg,
IMembershipOrg
} from '../../models';
import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables';
import _ from 'lodash';
export const getOrganizations = async (req: Request, res: Response) => {
let organizations;
@ -382,3 +384,44 @@ export const getOrganizationSubscriptions = async (
subscriptions
});
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req
* @param res
* @returns
*/
export const getOrganizationMembersAndTheirWorkspaces = async (
req: Request,
res: Response
) => {
const { organizationId } = req.params;
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString());
const memberships = (
await Membership.find({
workspace: { $in: workspacesSet }
}).populate('workspace')
);
const userToWorkspaceIds: any = {};
memberships.forEach(membership => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

View File

@ -8,7 +8,7 @@ import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { EMAIL_TOKEN_LIFETIME, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { BadRequestError } from '../../utils/errors';
/**
@ -39,7 +39,8 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
{
email,
token,
createdAt: new Date()
createdAt: new Date(),
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
},
{ upsert: true, new: true }
);

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
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 { completeAccount } from '../../helpers/user';
import {
@ -11,6 +11,7 @@ import {
import { issueTokens, createToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios';
import { BadRequestError } from '../../utils/errors';
/**
* 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 {
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');
if (user && user?.publicKey) {
// case: user has already completed account
@ -129,7 +138,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account

View File

@ -1,21 +1,21 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
IUser,
ServiceToken,
ServiceTokenData
} from '../../models';
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
IUser,
ServiceToken,
ServiceTokenData,
} from "../../models";
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import { addMemberships } from '../../helpers/membership';
import { ADMIN } from '../../variables';
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
/**
* Return public keys of members of workspace with id [workspaceId]
@ -24,32 +24,31 @@ import { ADMIN } from '../../variables';
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
let publicKeys;
try {
const { workspaceId } = req.params;
publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>('user', 'publicKey')
)
.map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace member public keys'
});
}
publicKeys = (
await Membership.find({
workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id,
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace member public keys",
});
}
return res.status(200).send({
publicKeys
});
return res.status(200).send({
publicKeys,
});
};
/**
@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users;
try {
const { workspaceId } = req.params;
let users;
try {
const { workspaceId } = req.params;
users = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace members'
});
}
users = await Membership.find({
workspace: workspaceId,
}).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace members",
});
}
return res.status(200).send({
users
});
return res.status(200).send({
users,
});
};
/**
@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces;
try {
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspaces'
});
}
let workspaces;
try {
workspaces = (
await Membership.find({
user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
return res.status(200).send({
workspaces
});
return res.status(200).send({
workspaces,
});
};
/**
@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
let workspace;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
_id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace'
});
}
workspace = await Workspace.findOne({
_id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({
workspace
});
return res.status(200).send({
workspace,
});
};
/**
@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
let workspace;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId,
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error('Workspace names must be at least 1-character long');
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId
});
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create workspace'
});
}
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
});
}
return res.status(200).send({
workspace
});
return res.status(200).send({
workspace,
});
};
/**
@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
// delete workspace
await deleteWork({
id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace'
});
}
// delete workspace
await deleteWork({
id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete workspace",
});
}
return res.status(200).send({
message: 'Successfully deleted workspace'
});
return res.status(200).send({
message: "Successfully deleted workspace",
});
};
/**
@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change workspace name'
});
}
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
},
{
name,
},
{
new: true,
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to change workspace name",
});
}
return res.status(200).send({
message: 'Successfully changed workspace name',
workspace
});
return res.status(200).send({
message: "Successfully changed workspace name",
workspace,
});
};
/**
@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations;
try {
const { workspaceId } = req.params;
let integrations;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integrations'
});
}
integrations = await Integration.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({
integrations
});
return res.status(200).send({
integrations,
});
};
/**
@ -281,56 +280,56 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
let authorizations;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integration authorizations'
});
}
authorizations = await IntegrationAuth.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integration authorizations",
});
}
return res.status(200).send({
authorizations
});
return res.status(200).send({
authorizations,
});
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
req: Request,
res: Response
) => {
let serviceTokens;
try {
const { workspaceId } = req.params;
// ?? FIX.
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service tokens'
});
}
return res.status(200).send({
serviceTokens
});
}
let serviceTokens;
try {
const { workspaceId } = req.params;
// ?? FIX.
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({
serviceTokens,
});
};

View File

@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
if (isReadBlocked) {
if (isReadBlocked && isWriteBlocked) {
return
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked
isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked
})
}
})

View File

@ -17,7 +17,9 @@ import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
/**
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
@ -284,8 +286,10 @@ export const getSecrets = async (req: Request, res: Response) => {
}
}
*/
const { workspaceId, environment } = req.query;
const { workspaceId, environment, tagSlugs } = req.query;
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
@ -298,16 +302,38 @@ export const getSecrets = async (req: Request, res: Response) => {
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) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let secrets: any
let secretQuery: any
const [err, secrets] = await to(Secret.find(
{
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
} else {
secretQuery = {
workspace: workspaceId,
environment,
$or: [
@ -316,9 +342,13 @@ export const getSecrets = async (req: Request, res: Response) => {
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).populate("tags").then())
}
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
if (hasWriteOnlyAccess) {
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
const channel = getChannelFromUserAgent(req.headers['user-agent'])
@ -356,6 +386,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)
* @param req

View File

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
Membership, Secret,
} from '../../models';
import Tag, { ITag } from '../../models/tag';
import { Builder } from "builder-pattern"
@ -54,6 +54,12 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const result = await Tag.findByIdAndDelete(tagId);
// remove the tag from secrets
await Secret.updateMany(
{ tags: { $in: [tagId] } },
{ $pull: { tags: tagId } }
);
res.json(result);
}

View File

@ -1,5 +1,6 @@
import _ from "lodash";
import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
@ -15,4 +16,39 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
}
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
}

View File

@ -7,6 +7,7 @@ import { createWorkspace } from './workspace';
import { addMemberships } from './membership';
import { OWNER, ADMIN, ACCEPTED } from '../variables';
import { sendMail } from '../helpers/nodemailer';
import { EMAIL_TOKEN_LIFETIME } from '../config';
/**
* Send magic link to verify email to [email]
@ -25,7 +26,8 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
{
email,
token,
createdAt: new Date()
createdAt: new Date(),
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
},
{ upsert: true, new: true }
);
@ -62,11 +64,20 @@ const checkEmailVerification = async ({
code: string;
}) => {
try {
const token = await Token.findOneAndDelete({
const token = await Token.findOne({
email,
token: code
});
if (token && Math.floor(Date.now() / 1000) > token.ttl) {
await Token.deleteOne({
email,
token: code
});
throw new Error('Verification token has expired')
}
if (!token) throw new Error('Failed to find email verification token');
} catch (err) {
Sentry.setUser(null);

View File

@ -1,7 +1,7 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import axios from "axios";
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -12,12 +12,14 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
/**
* Return list of names of apps for integration named [integration]
@ -29,7 +31,7 @@ import {
*/
const getApps = async ({
integrationAuth,
accessToken
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
@ -54,40 +56,45 @@ const getApps = async ({
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken
accessToken,
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
throw new Error("Failed to get integration apps");
}
return apps;
@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
apps = res.map((a: any) => ({
name: a.name
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps');
throw new Error("Failed to get Heroku integration apps");
}
return apps;
@ -131,10 +138,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({
const getAppsVercel = async ({
integrationAuth,
accessToken
}: {
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
@ -146,21 +153,23 @@ const getAppsVercel = async ({
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...( integrationAuth?.teamId ? {
params: {
teamId: integrationAuth.teamId
}
} : {})
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
throw new Error("Failed to get Vercel integration apps");
}
return apps;
@ -173,11 +182,7 @@ const getAppsVercel = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
@ -191,12 +196,12 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({
name: a.name,
appId: a.site_id
appId: a.site_id,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
throw new Error("Failed to get Netlify integration apps");
}
return apps;
@ -209,35 +214,32 @@ const getAppsNetlify = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
auth: accessToken,
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{
per_page: 100
}
)).data;
const repos = (
await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page: 100,
}
)
).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name,
owner: a.owner.login
})
);
name: a.name,
owner: a.owner.login,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
throw new Error("Failed to get Github repos");
}
return apps;
@ -251,11 +253,7 @@ const getAppsGithub = async ({
* @returns {String} apps.name - name of Render service
* @returns {String} apps.appId - id of Render service
*/
const getAppsRender = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
@ -263,8 +261,8 @@ const getAppsRender = async ({
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
}
'Accept-Encoding': 'application/json',
},
})
).data;
@ -277,11 +275,11 @@ const getAppsRender = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Render services');
throw new Error("Failed to get Render services");
}
return apps;
}
};
/**
* 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 {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const query = `
@ -308,34 +302,71 @@ const getAppsFlyio = async ({
}
}
`;
const res = (await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken,
const res = (
await axios({
url: INTEGRATION_FLYIO_API_URL,
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
},
data: {
query,
variables: {
role: null
}
}
})).data.data.apps.nodes;
apps = res
.map((a: any) => ({
name: a.name
}));
'Accept-Encoding': 'application/json',
},
data: {
query,
variables: {
role: null,
},
},
})
).data.data.apps.nodes;
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Fly.io apps');
throw new Error("Failed to get Fly.io 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 };

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -8,8 +8,9 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegration {
_id: Types.ObjectId;
@ -31,44 +32,47 @@ export interface IIntegration {
| 'netlify'
| 'github'
| 'render'
| 'flyio';
| 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId;
}
const integrationSchema = new Schema<IIntegration>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true
required: true,
},
isActive: {
type: Boolean,
required: true
required: true,
},
app: {
// name of app in provider
type: String,
default: null
default: null,
},
appId: { // (new)
appId: {
// (new)
// id of app in provider
type: String,
default: null
default: null,
},
targetEnvironment: { // (new)
// target environment
targetEnvironment: {
// (new)
// target environment
type: String,
default: null
default: null,
},
owner: {
// github-specific repo owner-login
type: String,
default: null
default: null,
},
path: {
// aws-parameter-store-specific path
@ -91,21 +95,22 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
],
required: true
required: true,
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
ref: "IntegrationAuth",
required: true,
},
},
{
timestamps: true
timestamps: true,
}
);
const Integration = model<IIntegration>('Integration', integrationSchema);
const Integration = model<IIntegration>("Integration", integrationSchema);
export default Integration;

View File

@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -8,24 +8,16 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| 'azure-key-vault'
| 'aws-parameter-store'
| '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
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
@ -41,9 +33,9 @@ export interface IIntegrationAuth {
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
integration: {
type: String,
@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
],
required: true
required: true,
},
teamId: {
// vercel-specific integration param
type: String
type: String,
},
accountId: {
// netlify-specific integration param
type: String
type: String,
},
refreshCiphertext: {
type: String,
select: false
select: false,
},
refreshIV: {
type: String,
select: false
select: false,
},
refreshTag: {
type: String,
select: false
select: false,
},
accessIdCiphertext: {
type: String,
@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
},
accessCiphertext: {
type: String,
select: false
select: false,
},
accessIV: {
type: String,
select: false
select: false,
},
accessTag: {
type: String,
select: false
select: false,
},
accessExpiresAt: {
type: Date,
select: false
}
select: false,
},
},
{
timestamps: true
timestamps: true,
}
);
const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth',
"IntegrationAuth",
integrationAuthSchema
);

View File

@ -109,6 +109,9 @@ const secretSchema = new Schema<ISecret>(
}
);
secretSchema.index({ tags: 1 }, { background: true })
const Secret = model<ISecret>('Secret', secretSchema);
export default Secret;

View File

@ -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;

View File

@ -5,6 +5,7 @@ export interface IToken {
email: string;
token: string;
createdAt: Date;
ttl: Number;
}
const tokenSchema = new Schema<IToken>({
@ -19,14 +20,13 @@ const tokenSchema = new Schema<IToken>({
createdAt: {
type: Date,
default: Date.now
},
ttl: {
type: Number,
}
});
tokenSchema.index({
createdAt: 1
}, {
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
});
tokenSchema.index({ email: 1 });
const Token = model<IToken>('Token', tokenSchema);

View File

@ -156,4 +156,19 @@ router.get(
organizationController.getOrganizationSubscriptions
);
router.get(
'/:organizationId/workspace-memberships',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationController.getOrganizationMembersAndTheirWorkspaces
);
export default router;

View File

@ -74,6 +74,7 @@ router.get(
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']

View File

@ -8,7 +8,7 @@
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>

View File

@ -7,7 +7,7 @@
</head>
<body>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>

View File

@ -3,8 +3,8 @@ import {
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
} from './environment';
ENV_SET,
} from "./environment";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -15,6 +15,7 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@ -27,27 +28,22 @@ import {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
} from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
ACTION_READ_SECRETS,
} from "./action";
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp";
import { PLAN_STARTER, PLAN_PRO } from "./stripe";
export {
OWNER,
@ -71,6 +67,7 @@ export {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@ -83,6 +80,7 @@ export {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,

View File

@ -3,50 +3,53 @@ import {
TENANT_ID_AZURE
} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL
} from '../config';
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL,
} from "../config";
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github";
const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
]);
// integration types
const INTEGRATION_OAUTH2 = 'oauth2';
const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
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_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token';
"https://github.com/login/oauth/access_token";
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [
{
@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [
clientId: '',
docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [
type: '',
clientId: '',
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_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
};

View File

@ -1,24 +1,16 @@
// membership roles
const OWNER = 'owner';
const ADMIN = 'admin';
const MEMBER = 'member';
const OWNER = "owner";
const ADMIN = "admin";
const MEMBER = "member";
// membership statuses
const INVITED = 'invited';
const INVITED = "invited";
// membership permissions ability
const ABILITY_READ = 'read';
const ABILITY_WRITE = 'write';
const ABILITY_READ = "read";
const ABILITY_WRITE = "write";
// -- organization
const ACCEPTED = 'accepted';
const ACCEPTED = "accepted";
export {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
}
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };

4
cli/docker/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM alpine
RUN apk add --no-cache tini
COPY infisical /bin/infisical
ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"]

View File

@ -114,6 +114,7 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
@ -154,13 +155,12 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
if err != nil {
return false
}
if response.IsError() {
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
return false
}
@ -175,8 +175,6 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
if err != nil {
return GetAccessibleEnvironmentsResponse{}, err
}

View File

@ -197,6 +197,7 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
}
type GetEncryptedSecretsV2Response struct {

View File

@ -61,7 +61,12 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@ -97,6 +102,7 @@ func init() {
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
}
// Format according to the format flag

View File

@ -56,7 +56,7 @@ var initCmd = &cobra.Command{
workspaces := workspaceResponse.Workspaces
if len(workspaces) == 0 {
message := fmt.Sprintf("You don't have any projects created in Infisical. You must first create a project at %s", util.INFISICAL_TOKEN_NAME)
util.PrintMessageAndExit(message)
util.PrintErrorMessageAndExit(message)
}
var workspaceNames []string

View File

@ -4,7 +4,6 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
"fmt"
"os"
"github.com/Infisical/infisical-merge/packages/util"
@ -27,8 +26,6 @@ var resetCmd = &cobra.Command{
util.HandleError(err)
}
fmt.Println(pathToDir)
os.RemoveAll(pathToDir)
// delete keyring

View File

@ -74,7 +74,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@ -148,6 +153,7 @@ func init() {
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
}
// Will execute a single command and pass in the given secrets into the process

View File

@ -46,7 +46,12 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err)
}
@ -87,15 +92,13 @@ var secretsSetCmd = &cobra.Command{
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if !util.IsSecretEnvironmentValid(environmentName) {
util.PrintMessageAndExit("You have entered a invalid environment name", "Environment names can only be prod, dev, test or staging")
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get your local config details")
@ -148,11 +151,11 @@ var secretsSetCmd = &cobra.Command{
for _, arg := range args {
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" {
util.PrintMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again")
util.PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again")
}
if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) {
util.PrintMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again")
util.PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again")
}
// Key and value from argument
@ -303,7 +306,7 @@ var secretsDeleteCmd = &cobra.Command{
if len(invalidSecretNamesThatDoNotExist) != 0 {
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
util.PrintMessageAndExit(message)
util.PrintErrorMessageAndExit(message)
}
request := api.BatchDeleteSecretsBySecretIdsRequest{
@ -332,17 +335,17 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
util.HandleError(err, "Unable to parse flag")
}
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
@ -375,22 +378,27 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
util.HandleError(err, "Unable to parse flag")
}
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
tagsHashToSecretKey := make(map[string]int)
slugsToFilerBy := make(map[string]int)
for _, slug := range strings.Split(tagSlugs, ",") {
slugsToFilerBy[slug] = 1
}
type TagsAndSecrets struct {
Secrets []models.SingleEnvironmentVariable
@ -407,6 +415,25 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
return len(secrets[i].Tags) > len(secrets[j].Tags)
})
for i, secret := range secrets {
filteredTag := []struct {
ID string "json:\"_id\""
Name string "json:\"name\""
Slug string "json:\"slug\""
Workspace string "json:\"workspace\""
}{}
for _, secretTag := range secret.Tags {
_, exists := slugsToFilerBy[secretTag.Slug]
if !exists {
filteredTag = append(filteredTag, secretTag)
}
}
secret.Tags = filteredTag
secrets[i] = secret
}
for _, secret := range secrets {
listOfTagSlugs := []string{}
@ -470,6 +497,8 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags)
})
tableOfContents := []string{}
fullyGeneratedDocuments := []string{}
for _, secretDetails := range listOfsecretDetails {
listOfKeyValue := []string{}
@ -510,11 +539,22 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
heading := CenterString(strings.Join(listOfTagNames, " & "), 80)
if len(listOfTagNames) == 0 {
fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
fullyGeneratedDocuments = append(fullyGeneratedDocuments, fmt.Sprintf("\n%s \n", strings.Join(listOfKeyValue, "\n")))
} else {
fmt.Printf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
fullyGeneratedDocuments = append(fullyGeneratedDocuments, fmt.Sprintf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n")))
tableOfContents = append(tableOfContents, strings.ToUpper(strings.Join(listOfTagNames, " & ")))
}
}
dashedList := []string{}
for _, item := range tableOfContents {
dashedList = append(dashedList, fmt.Sprintf("# - %s \n", item))
}
if len(dashedList) > 0 {
fmt.Println(CenterString("TABLE OF CONTENTS", 80))
fmt.Println(strings.Join(dashedList, ""))
}
fmt.Println(strings.Join(fullyGeneratedDocuments, ""))
}
func CenterString(s string, numStars int) string {
@ -567,5 +607,6 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
rootCmd.AddCommand(secretsCmd)
}

View File

@ -39,7 +39,8 @@ type Workspace struct {
}
type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"`
WorkspaceId string `json:"workspaceId"`
DefaultEnvironment string `json:"defaultEnvironment"`
}
type SymmetricEncryptionResult struct {
@ -51,4 +52,5 @@ type SymmetricEncryptionResult struct {
type GetAllSecretsParameters struct {
Environment string
InfisicalToken string
TagSlugs string
}

View File

@ -65,29 +65,29 @@ func RequireLogin() {
}
if !currentUserDetails.IsUserLoggedIn {
PrintMessageAndExit("You must be logged in to run this command. To login, run [infisical login]")
PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]")
}
if currentUserDetails.LoginExpired {
PrintMessageAndExit("Your login expired, please login in again. To login, run [infisical login]")
PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]")
}
if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" {
PrintMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]")
PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]")
}
}
func RequireServiceToken() {
serviceToken := os.Getenv(INFISICAL_TOKEN_NAME)
if serviceToken == "" {
PrintMessageAndExit("No service token is found in your terminal")
PrintErrorMessageAndExit("No service token is found in your terminal")
}
}
func RequireLocalWorkspaceFile() {
workspaceFileExists := WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
PrintMessageAndExit("It looks you have not yet connected this project to Infisical", "To do so, run [infisical init] then run your command again")
PrintErrorMessageAndExit("It looks you have not yet connected this project to Infisical", "To do so, run [infisical init] then run your command again")
}
workspaceFile, err := GetWorkSpaceFromFile()
@ -96,7 +96,7 @@ func RequireLocalWorkspaceFile() {
}
if workspaceFile.WorkspaceId == "" {
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
PrintErrorMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
}
}

View File

@ -31,10 +31,10 @@ func PrintSuccessMessage(message string) {
color.New(color.FgGreen).Println(message)
}
func PrintMessageAndExit(messages ...string) {
func PrintErrorMessageAndExit(messages ...string) {
if len(messages) > 0 {
for _, message := range messages {
fmt.Println(message)
fmt.Fprintln(os.Stderr, message)
}
}
@ -42,5 +42,5 @@ func PrintMessageAndExit(messages ...string) {
}
func printError(e error) {
color.Red("Hmm, we ran into an error: %v", e)
color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v", e)
}

View File

@ -62,7 +62,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return plainTextSecrets, nil
}
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@ -85,6 +85,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: workspaceId,
Environment: environmentName,
TagSlugs: tagSlugs,
})
if err != nil {
@ -130,13 +131,17 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, err
}
if workspaceFile.DefaultEnvironment != "" {
params.Environment = workspaceFile.DefaultEnvironment
}
// Verify environment
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
if err != nil {
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs)
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]

View File

@ -0,0 +1,11 @@
---
title: "infisical reset"
description: "Reset Infisical"
---
```bash
infisical reset
```
## Description
This command provides a way to clear all Infisical-generated configuration data, effectively resetting the software to its default settings. This can be an effective way to address any persistent issues that arise while using the CLI.

View File

@ -114,7 +114,8 @@
"cli/commands/run",
"cli/commands/secrets",
"cli/commands/export",
"cli/commands/vault"
"cli/commands/vault",
"cli/commands/reset"
]
},
"cli/faq"

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_NETLIFY` | OAuth2 client secret for Netlify 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` |
| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` |

View File

@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
'netlify': 'Netlify',
'github': 'GitHub',
'render': 'Render',
'flyio': 'Fly.io'
'flyio': 'Fly.io',
"circleci": 'CircleCI'
}
const envMapping: Mapping = {

View File

@ -10,7 +10,7 @@ export interface Tag {
export interface SecretDataProps {
pos: number;
key: string;
value: string;
value: string | undefined;
valueOverride: string | undefined;
id: string;
comment: string;

View File

@ -1,6 +1,6 @@
{
"support": {
"slack": "[NEW] Join Slack Forum",
"slack": "Join Slack Forum",
"docs": "Read Docs",
"issue": "Open a Github Issue",
"email": "Send us an Email"

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
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 { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
ability: "read",
environmentSlug: slug
}];
} else if (val === "Add Only") {
denials = [{
ability: "read",
environmentSlug: slug
}];
} else {
denials = [];
}
@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
return (
<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
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text="You can change user permissions if you switch to Infisical's Professional plan."
/>
<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>
<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-6 pr-10 py-3.5">ROLE</th>
{workspaceEnvs.map(env => (
<th key={guidGenerator()} className="text-left pl-8 py-1 max-w-min break-normal">
<span>{env.name.toUpperCase()}<br/></span>
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
<span>{env.slug.toUpperCase()}<br/></span>
{/* <span>PERMISSION</span> */}
</th>
))}
@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
user.email?.toLowerCase().includes(filter)
)
.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">
{row.firstName} {row.lastName}
</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">
<div className="justify-start h-full flex flex-row items-center">
<Select
className="w-36"
className="w-36 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
// open={isOpen}
onValueChange={(e) => handleRoleUpdate(index, e)}
value={row.role}
@ -253,23 +259,36 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)}
</div>
</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
className="w-36"
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen}
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
value={
// 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"))
? "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'}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="No Access">No Access</SelectItem>
<SelectItem value="Read Only">Read Only</SelectItem>
<SelectItem value="Read & Write">Read & Write</SelectItem>
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
</Select>
</td>)}
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">

View File

@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
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 getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
@ -36,6 +37,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
);
const router = useRouter();
const [myRole, setMyRole] = useState('member');
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
@ -79,6 +81,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
(async () => {
const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))})
setUserProjectMemberships(result);
})();
}, [userData, myUser]);
const grantAccess = async (id: string, publicKey: string) => {
@ -110,7 +116,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
};
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" />
<table className="w-full my-0.5">
<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">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">PROJECTS</th>
<th aria-label="buttons" />
</tr>
</thead>
@ -189,6 +196,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
)}
</div>
</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">
{myUser !== row.email &&
// row.role !== "admin" &&

View File

@ -1,5 +1,5 @@
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';
type NotificationType = 'success' | 'error' | 'info';
@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
return (
<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"
>
{notification.type === 'error' && (
@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
{notification.type === 'info' && (
<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
type="button"
className="rounded-lg"
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>
</div>
);

View File

@ -1,9 +1,10 @@
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 guidGenerator from '../utilities/randomId';
import { HoverObject } from '../v2/HoverCard';
import { PopoverObject } from '../v2/Popover/Popover';
const REGEX = /([$]{.*?})/g;
@ -112,7 +113,7 @@ const DashboardInputField = ({
}}>
<HoverObject
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'}
/>
</button>
@ -125,24 +126,24 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate;
return (
<div title={value} className={`relative flex-col w-full h-10 ${
isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}>
<div
className={`group relative flex flex-col justify-center items-center ${
error ? 'w-max' : 'w-full'
}`}
>
<input
onChange={(e) => onChangeHandler(e.target.value, position)}
type={type}
value={value}
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'
spellCheck="false"
placeholder=''
/>
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}>
<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 h-full ${
error ? 'w-max' : 'w-full'
}`}
>
{value?.split("\n")[0] ? <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
{value?.split("\n")[0]}
</span> : <span className='text-bunker-400'>-</span> }
{value?.split("\n")[1] && <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
{value?.split("\n")[1]}
</span>}
</div>
</div>
</div>
</PopoverObject>
);
}
if (type === 'value') {
@ -215,7 +216,7 @@ const DashboardInputField = ({
))}
{value?.split('').length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
</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>

View File

@ -132,7 +132,7 @@ const KeyPair = ({
/>
</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">
<DashboardInputField
onChangeHandler={modifyComment}
@ -171,12 +171,26 @@ const KeyPair = ({
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
</div>
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
<DeleteActionButton
{keyPair.key || keyPair.value
? <DeleteActionButton
onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}}
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>

View File

@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps {
key: string;
value: string;
value: string | undefined;
valueOverride: string | undefined;
pos: number;
id: string;
@ -80,9 +80,9 @@ const SideBar = ({
const { t } = useTranslation();
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 ? (
<div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center h-full w-full">
<Image
src="/images/loading/loading.gif"
height={60}
@ -91,7 +91,7 @@ const SideBar = ({
/>
</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">
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
<div
@ -186,7 +186,7 @@ const SideBar = ({
/>
</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>
<Button
text="Compare secret across environments"
@ -197,7 +197,7 @@ const SideBar = ({
<CompareSecretsModal
compareModal={compareModal}
setCompareModal={setCompareModal}
currentSecret={{ key: data[0]?.key, value: data[0]?.value }}
currentSecret={{ key: data[0]?.key, value: data[0]?.value ?? '' }}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv}
workspaceId={workspaceId}

View File

@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0}
className={`relative ${
cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer'
? 'cursor-pointer duration-200 hover:bg-white/10'
: '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={() => {
if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption);
@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo"
/>
{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 className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</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}
</div>
)}
{cloudIntegrationOption.isAvailable &&
integrationAuths
.map((authorization) => authorization.integration)
.map((authorization) => authorization?.integration)
.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
onKeyDown={() => null}
role="button"
@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths
.filter(
(authorization) =>
authorization.integration === cloudIntegrationOption.slug
(authorization) => authorization.integration === cloudIntegrationOption.slug
)
.map((authorization) => authorization._id)[0]
});
@ -96,20 +95,20 @@ const CloudIntegration = ({
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
</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">
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
<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="mr-2 text-xs" />
Authorized
</div>
</div>
)}
{!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 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="group absolute top-0 right-0 z-50 flex flex-row">
<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
</div>
</div>

View File

@ -45,8 +45,8 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void;
};
const IntegrationTile = ({
integration,
const IntegrationTile = ({
integration,
integrations,
bot,
setBot,
@ -57,7 +57,7 @@ const IntegrationTile = ({
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || {
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
slug: ''
}
@ -69,11 +69,10 @@ const IntegrationTile = ({
useEffect(() => {
const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth
integrationAuthId: integration?.integrationAuth
});
setApps(tempApps);
if (integration?.app) {
@ -90,15 +89,16 @@ const IntegrationTile = ({
case 'vercel':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
: 'Development'
? integration.targetEnvironment.charAt(0).toUpperCase() +
integration.targetEnvironment.substring(1)
: 'Development'
);
break;
case 'netlify':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
);
break;
default:
@ -108,7 +108,7 @@ const IntegrationTile = ({
loadIntegration();
}, []);
const handleStartIntegration = async () => {
const reformatTargetEnvironment = (targetEnvironment: string) => {
switch (integration.integration) {
@ -119,13 +119,13 @@ const IntegrationTile = ({
default:
return null;
}
}
};
try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const appId = siteApp?.appId ?? null;
const owner = siteApp?.owner ?? null;
// return updated integration
const updatedIntegration = await updateIntegration({
integrationId: integration._id,
@ -136,15 +136,15 @@ const IntegrationTile = ({
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
owner
});
setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
);
} catch (err) {
console.error(err);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => {
try {
@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel':
return (
<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
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment}
@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify':
return (
<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
data={
!integration.isActive
@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />;
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>
<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
data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name}
@ -208,7 +208,7 @@ const IntegrationTile = ({
/>
</div>
<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 className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@ -218,7 +218,7 @@ const IntegrationTile = ({
</div>
</div>
<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
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
@ -231,9 +231,9 @@ const IntegrationTile = ({
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
<div className="text-gray-300 font-semibold">In Sync</div>
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div>
</div>
) : (
<Button
@ -243,11 +243,13 @@ const IntegrationTile = ({
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
onButtonPressed={() => handleDeleteIntegration({
integration
})}
onButtonPressed={() =>
handleDeleteIntegration({
integration
})
}
color="red"
size="icon-md"
icon={faX}

View File

@ -26,7 +26,7 @@ interface Integration {
}
const ProjectIntegrationSection = ({
integrations,
integrations,
setIntegrations,
bot,
setBot,
@ -35,22 +35,20 @@ const ProjectIntegrationSection = ({
}: Props) => {
return integrations.length > 0 ? (
<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">
<h1 className="font-semibold text-3xl">Current Integrations</h1>
<p className="text-base text-gray-400">
Manage integrations with third-party services.
</p>
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
</div>
{integrations.map((integration: Integration) => {
return (
<IntegrationTile
key={`integration-${integration._id.toString()}`}
integration={integration}
key={`integration-${integration?._id.toString()}`}
integration={integration}
integrations={integrations}
bot={bot}
setBot={setBot}
setIntegrations={setIntegrations}
environments={environments}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
);
@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : (
<div />
);
}
export default ProjectIntegrationSection;
};
export default ProjectIntegrationSection;

View File

@ -1,10 +1,7 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useWorkspace } from '@app/context';
import getOrganization from '@app/pages/api/organization/GetOrg';
import { useOrganization, useWorkspace } from '@app/context';
/**
* This is the component at the top of almost every page.
@ -22,28 +19,15 @@ export default function NavHeader({
pageName: string;
isProjectRelated?: boolean;
}): JSX.Element {
const [orgName, setOrgName] = useState('');
const router = useRouter();
const projectId = String(router.query.id);
const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id');
const org = await getOrganization({
orgId: orgId || ''
});
setOrgName(org.name);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
const { currentOrg } = useOrganization();
return (
<div className="ml-6 flex flex-row items-center pt-8">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
{orgName?.charAt(0)}
{currentOrg?.name?.charAt(0)}
</div>
<div className="text-sm font-semibold text-primary">{orgName}</div>
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
{isProjectRelated && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />

View File

@ -76,7 +76,7 @@ const encryptSecrets = async ({
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric({
plaintext: secret.value,
plaintext: secret.value ?? '',
key: randomBytes
});

View File

@ -24,7 +24,7 @@ interface EncryptedSecretProps {
interface SecretProps {
key: string;
value: string;
value: string | undefined;
type: 'personal' | 'shared';
comment: string;
id: string;
@ -87,12 +87,17 @@ const getSecretsForProject = async ({
key
});
const plainTextValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
let plainTextValue;
if (secret.secretValueCiphertext !== undefined) {
plainTextValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
} else {
plainTextValue = undefined;
}
let plainTextComment;
if (secret.secretCommentCiphertext) {

View File

@ -38,6 +38,13 @@ export const Secondary: Story = {
}
};
export const Star: Story = {
args: {
children: 'Hello Infisical',
variant: 'star'
}
};
export const Danger: Story = {
args: {
children: 'Hello Infisical',

View File

@ -24,14 +24,16 @@ const buttonVariants = cva(
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
plain: '',
// a constant color not in use on hover or click goes colorSchema color
star: 'text-bunker-200 bg-mineshaft-500'
},
isDisabled: {
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
@ -53,6 +55,16 @@ const buttonVariants = cva(
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'star',
className: 'hover:bg-primary hover:text-black'
},
{
colorSchema: 'danger',
variant: 'star',
className: 'hover:bg-red hover:text-white'
},
{
colorSchema: 'primary',
variant: 'outline',

View File

@ -8,9 +8,9 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);

View File

@ -28,8 +28,9 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root
className={twMerge(
'flex items-center justify-center w-5 h-5 mr-3 transition-all rounded shadow hover:bg-bunker-200 bg-bunker-300',
'flex items-center justify-center w-4 h-4 mr-3 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
isChecked && 'bg-primary hover:bg-primary',
className
)}
required={isRequired}

View File

@ -24,7 +24,7 @@ export const HoverObject = ({
</a>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent z-[50]" sideOffset={5}>
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
<div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<div>

View File

@ -31,18 +31,20 @@ export const MenuItem = <T extends ElementType = 'button'>({
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef
inputRef,
...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
<li
className={twMerge(
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
isSelected && 'text-primary',
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
'px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-600 duration-50',
isSelected && 'bg-mineshaft-500',
isDisabled && 'hover:bg-transparent cursor-not-allowed',
className
)}
>
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
{icon && <span className="mr-2">{icon}</span>}
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
{icon && <span className="mr-3 ml-4 w-5">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}

View File

@ -25,7 +25,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<Card
isRounded
className={twMerge(
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
className
)}
>
@ -36,7 +36,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
>
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
</IconButton>

View File

@ -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';

View File

@ -0,0 +1,2 @@
export type { PopoverProps } from './Popover';
export { PopoverObject } from './Popover';

View File

@ -1,4 +1,5 @@
import { forwardRef, ReactNode } from 'react';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as SelectPrimitive from '@radix-ui/react-select';
@ -12,13 +13,15 @@ type Props = {
className?: string;
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
icon?: IconProp;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
ref
): JSX.Element => {
return (
@ -31,7 +34,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
className
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
{!props.disabled && (
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
@ -41,14 +46,16 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
'relative left-4 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
)}
position={position}
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1.5">
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
@ -73,6 +80,7 @@ Select.displayName = 'Select';
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
isDisabled?: boolean;
isSelected?: boolean;
customIcon?: IconProp;
};
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
@ -82,16 +90,16 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
{...props}
className={twMerge(
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 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`,
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
)}
ref={forwardedRef}
>
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
<FontAwesomeIcon icon={faCheck} size="sm" />
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
<FontAwesomeIcon icon={props.customIcon ? props.customIcon : faCheck} />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>

View File

@ -44,3 +44,5 @@ const plansProd: Mapping = {
};
export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';

View File

@ -0,0 +1,46 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetOrganization } from '@app/hooks/api';
import { Organization } from '@app/hooks/api/types';
import { useWorkspace } from '../WorkspaceContext';
type TOrgContext = {
orgs?: Organization[];
currentOrg?: Organization;
isLoading: boolean;
};
const OrgContext = createContext<TOrgContext | null>(null);
type Props = {
children: ReactNode;
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { currentWorkspace } = useWorkspace();
const { data: userOrgs, isLoading } = useGetOrganization();
const currentWsOrgID = currentWorkspace?.organization;
// memorize the workspace details for the context
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
);
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
};
export const useOrganization = () => {
const ctx = useContext(OrgContext);
if (!ctx) {
throw new Error('useOrganization to be used within <OrgContext.Provider>');
}
return ctx;
};

View File

@ -0,0 +1 @@
export { OrgProvider, useOrganization } from './OrganizationContext';

View File

@ -0,0 +1,38 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetUser } from '@app/hooks/api';
import { User } from '@app/hooks/api/types';
type TUserContext = {
user: User;
isLoading: boolean;
};
const UserContext = createContext<TUserContext | null>(null);
type Props = {
children: ReactNode;
};
export const UserProvider = ({ children }: Props): JSX.Element => {
const { data, isLoading } = useGetUser();
// memorize the workspace details for the context
const value = useMemo<TUserContext>(() => {
return {
user: data!,
isLoading
};
}, [data, isLoading]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) {
throw new Error('useUser has to be used within <UserContext.Provider>');
}
return ctx;
};

View File

@ -0,0 +1 @@
export { UserProvider, useUser } from './UserContext';

View File

@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
// memorize the workspace details for the context
const value = useMemo<TWorkspaceContext>(() => {
const wsId = workspaceId || localStorage.getItem('projectData.id');
return {
workspaces: ws || [],
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
isLoading
};
}, [ws, workspaceId, isLoading]);

View File

@ -1,2 +1,5 @@
export { AuthProvider } from './AuthContext';
export { OrgProvider, useOrganization } from './OrganizationContext';
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
export { UserProvider, useUser } from './UserContext';
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';

View File

@ -2,14 +2,13 @@ import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getActionData from '@app/ee/api/secrets/GetActionData';
import patienceDiff from '@app/ee/utilities/findTextDifferences';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
import {
decryptAssymmetric,
decryptSymmetric
@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div
className={`absolute border-l border-mineshaft-500 ${
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 ? (
<div className="flex items-center justify-center h-full mb-8">
@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
/>
</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">
<p className="font-semibold text-lg text-bunker-200">
{t(`activity:event.${actionMetaData?.name}`)}
@ -154,26 +153,21 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
tabIndex={0}
onClick={() => toggleSidebar('')}
>
<FontAwesomeIcon icon={faX} 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 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 === 'addSecrets' ||
actionMetaData?.name === 'deleteSecrets') &&
actionData?.map((item, id) => (
<div key={`secret.${id + 1}`}>
<div className="text-xs text-bunker-200 mt-4 pl-1">
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
{item.newSecretVersion.key}
</div>
<DashboardInputField
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
<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'>
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
</div>
</div>
))}
{actionMetaData?.name === 'updateSecrets' &&
@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key}
</div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
<div className="bg-red/30 px-2">
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
<div className="bg-red/40 px-2 ph-no-capture">
-{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.bIndex !== -1 && (
<span
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}
</span>
)
)}
</div>
<div className="bg-green-500/30 px-2">
<div className="break-all bg-green-500/40 px-2 ph-no-capture">
+{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.aIndex !== -1 && (
<span
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}
</span>

View File

@ -159,9 +159,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
return (
<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'
} 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 ? (
<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" />
</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) => (
<div
onKeyDown={() => null}

View File

@ -76,9 +76,9 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
}, [secretId]);
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>
<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 ? (
<div className="flex items-center justify-center h-full">
<Image
@ -99,10 +99,10 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
<div className="p-1">
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
</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 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', {
year: 'numeric',
month: '2-digit',
@ -113,11 +113,11 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
})}
</div>
<div className="">
<p className="break-words">
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
<p className="break-words ph-no-capture">
<span className="py-0.5 px-1 rounded-sm bg-primary-500/30 mr-1.5">
Value:
</span>
{version.value}
<span className='font-mono'>{version.value}</span>
</p>
</div>
</div>

View File

@ -1,6 +1,8 @@
export * from './auth';
export * from './keys';
export * from './organization';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
export * from './users';
export * from './workspace';

View File

@ -1 +1 @@
export { useGetUserWsKey } from './queries';
export { useGetUserWsKey, useUploadWsKey } from './queries';

View File

@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { UserWsKeyPair } from './types';
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
// mutations
export const useUploadWsKey = () =>
useMutation<{}, {}, UploadWsKeyDTO>({
mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
});

View File

@ -20,3 +20,10 @@ export type Sender = {
lastName: string;
publicKey: string;
};
export type UploadWsKeyDTO = {
userId: string;
encryptedKey: string;
nonce: string;
workspaceId: string;
};

View File

@ -0,0 +1 @@
export { useGetOrganization } from './queries';

View File

@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { Organization } from './types';
const organizationKeys = {
getUserOrganization: ['organization'] as const
};
const fetchUserOrganization = async () => {
const { data } = await apiRequest.get<{ organizations: Organization[] }>('/api/v1/organization');
return data.organizations;
};
export const useGetOrganization = () =>
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });

View File

@ -0,0 +1,6 @@
export type Organization = {
_id: string;
name: string;
createAt: string;
updatedAt: string;
};

View File

@ -1,7 +1,9 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
export type { User } from './users/types';
export type {
CreateEnvironmentDTO,
DeleteEnvironmentDTO,

View File

@ -0,0 +1,7 @@
export {
fetchOrgUsers,
useAddUserToWs,
useGetOrgUsers,
useGetUser,
useLogoutUser
} from './queries';

View File

@ -0,0 +1,84 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import {
decryptAssymmetric,
encryptAssymmetric
} from '@app/components/utilities/cryptography/crypto';
import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { useUploadWsKey } from '../keys/queries';
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
const userKeys = {
getUser: ['user'] as const,
getOrgUsers: (orgId: string) => [{ orgId }, 'user']
};
const fetchUserDetails = async () => {
const { data } = await apiRequest.get<{ user: User }>('/api/v1/user');
return data.user;
};
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
export const fetchOrgUsers = async (orgId: string) => {
const { data } = await apiRequest.get<{ users: OrgUser[] }>(
`/api/v1/organization/${orgId}/users`
);
return data.users;
};
export const useGetOrgUsers = (orgId: string) =>
useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
// mutation
export const useAddUserToWs = () => {
const uploadWsKey = useUploadWsKey();
return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
mutationFn: ({ email, workspaceId }) =>
apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }),
onSuccess: ({ data }, { workspaceId }) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: data.latestKey.encryptedKey,
nonce: data.latestKey.nonce,
publicKey: data.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
plaintext: key,
publicKey: data.invitee.publicKey,
privateKey: PRIVATE_KEY
});
uploadWsKey.mutate({
encryptedKey: inviteeCipherText,
nonce: inviteeNonce,
userId: data.invitee._id,
workspaceId
});
}
});
};
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
onSuccess: () => {
setAuthToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem('publicKey', '');
localStorage.setItem('encryptedPrivateKey', '');
localStorage.setItem('iv', '');
localStorage.setItem('tag', '');
localStorage.setItem('PRIVATE_KEY', '');
}
});

View File

@ -0,0 +1,39 @@
import { UserWsKeyPair } from '../keys/types';
export type User = {
seenIps: string[];
_id: string;
email: string;
createdAt: Date;
updatedAt: Date;
__v: number;
firstName: string;
lastName: string;
publicKey: string;
};
export type OrgUser = {
_id: string;
user: {
email: string;
firstName: string;
lastName: string;
_id: string;
publicKey: string;
};
inviteEmail: string;
organization: string;
role: 'owner' | 'admin' | 'member';
status: 'invited' | 'accepted';
deniedPermissions: any[];
};
export type AddUserToWsDTO = {
workspaceId: string;
email: string;
};
export type AddUserToWsRes = {
invitee: OrgUser['user'];
latestKey: UserWsKeyPair;
};

View File

@ -1,4 +1,5 @@
export {
useCreateWorkspace,
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,

View File

@ -4,6 +4,7 @@ import { apiRequest } from '@app/config/request';
import {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
@ -40,6 +41,18 @@ export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
// mutation
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, workspaceName }) =>
apiRequest.post('/api/v1/workspace', { workspaceName, organizationId }),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useRenameWorkspace = () => {
const queryClient = useQueryClient();

View File

@ -11,6 +11,11 @@ export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
export type CreateWorkspaceDTO = {
workspaceName: string;
organizationId: string;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };

View File

@ -1,2 +1,3 @@
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
export { useLeaveConfirm } from './useLeaveConfirm';

View File

@ -0,0 +1,55 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { leaveConfirmDefaultMessage } from '@app/const';
type LeaveConfirmProps = {
initialValue: boolean,
message?: string
}
interface LeaveConfirmReturn {
hasUnsavedChanges: boolean,
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
}
export function useLeaveConfirm({
initialValue,
message = leaveConfirmDefaultMessage,
}: LeaveConfirmProps): LeaveConfirmReturn {
const router = useRouter()
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
const onRouteChangeStart = useCallback(() => {
if (hasUnsavedChanges) {
if (window.confirm(message)) {
return true
}
throw new Error("Abort route change by user's confirmation.")
}
return false;
}, [hasUnsavedChanges])
const handleWindowClose = useCallback((e: any) => {
if (!hasUnsavedChanges) {
return;
}
e.preventDefault();
e.returnValue = message;
}, []);
useEffect(() => {
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener('beforeunload', handleWindowClose);
return () => {
router.events.off("routeChangeStart", onRouteChangeStart);
window.removeEventListener('beforeunload', handleWindowClose);
}
}, [onRouteChangeStart, handleWindowClose]);
return {
hasUnsavedChanges,
setHasUnsavedChanges,
};
}

View File

@ -0,0 +1,455 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
import crypto from 'crypto';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
faBookOpen,
faFileLines,
faGear,
faKey,
faMobile,
faPlug,
faPlus,
faUser
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
import { tempLocalStorage } from '@app/components/utilities/checks/tempLocalStorage';
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
import {
Button,
Checkbox,
FormControl,
Input,
Menu,
MenuItem,
Modal,
ModalContent,
Select,
SelectItem
} from '@app/components/v2';
import { useOrganization, useUser, useWorkspace } from '@app/context';
import { usePopUp } from '@app/hooks';
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from '@app/hooks/api';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import { Navbar } from './components/NavBar';
interface LayoutProps {
children: React.ReactNode;
}
const formSchema = yup.object({
name: yup.string().required().label('Project Name').trim(),
addMembers: yup.bool().required().label('Add Members')
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
export const AppLayout = ({ children }: LayoutProps) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
// eslint-disable-next-line prefer-const
let { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
workspaces = workspaces.filter((ws) => ws.organization === currentOrg?._id);
const { user } = useUser();
const createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey();
const addWsUser = useAddUserToWs();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
'addNewWs'
] as const);
const {
control,
formState: { isSubmitting },
reset,
handleSubmit
} = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema)
});
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
const { t } = useTranslation();
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]?._id);
}
const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage('orgData.id')
});
const userWorkspaces = orgUserProjects;
if (
(userWorkspaces.length === 0 &&
router.asPath !== '/noprojects' &&
!router.asPath.includes('home') &&
!router.asPath.includes('settings')) ||
router.asPath === '/dashboard/undefined'
) {
router.push('/noprojects');
} else if (router.asPath !== '/noprojects') {
const intendedWorkspaceId = router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
) {
router.push(`/dashboard/${userWorkspaces[0]._id}`);
} else {
setWorkspaceMapping(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
) as any
);
setWorkspaceSelected(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
)[router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]]
);
}
}
};
putUserInWorkSpace();
onboardingCheck({ setTotalOnboardingActionsDone });
}, [router.query.id]);
useEffect(() => {
try {
if (
workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !==
router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]
) {
localStorage.setItem('projectData.id', `${workspaceMapping[workspaceSelected as any]}`);
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
}
} catch (err) {
console.log(err);
}
}, [workspaceSelected]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
if (!currentOrg?._id) return;
try {
const {
data: {
workspace: { _id: newWorkspaceId }
}
} = await createWs.mutateAsync({
organizationId: currentOrg?._id,
workspaceName: name
});
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
encryptedKey: ciphertext,
nonce,
userId: user?._id,
workspaceId: newWorkspaceId
});
if (addMembers) {
console.log('adding other users');
// not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg._id);
orgUsers.forEach(({ status, user: orgUser }) => {
// skip if status of org user is not accepted
// this orgUser is the person who created the ws
if (status !== 'accepted' || user.email === orgUser.email) return;
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId });
});
}
createNotification({ text: 'Workspace created', type: 'success' });
handlePopUpClose('addNewWs');
router.push(`/dashboard/${newWorkspaceId}`);
} catch (err) {
console.error(err);
createNotification({ text: 'Failed to create workspace', type: 'error' });
}
};
return (
<>
<div className="hidden h-screen w-full flex-col overflow-x-hidden md:flex dark">
<Navbar />
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
<div>
{currentWorkspace ? (
<div className="w-full p-4 mt-3 mb-4">
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">
Project
</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
) : (
<div className="w-full p-4 mt-3 mb-4">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
)}
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
<Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/dashboard/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faKey} size="lg" />}
>
{t('nav:menu.secrets')}
</MenuItem>
</a>
</Link>
<Link href={`/users/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/users/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faUser} size="lg" />}
>
{t('nav:menu.members')}
</MenuItem>
</a>
</Link>
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faPlug} size="lg" />}
>
{t('nav:menu.integrations')}
</MenuItem>
</a>
</Link>
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
>
Activity Logs
</MenuItem>
</a>
</Link>
<Link href={`/settings/project/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/settings/project/${currentWorkspace?._id}`
}
icon={<FontAwesomeIcon icon={faGear} size="lg" />}
>
{t('nav:menu.project-settings')}
</MenuItem>
</a>
</Link>
</Menu>
</div>
</div>
<div className="mt-40 mb-4 w-full px-2">
{router.asPath.split('/')[1] === 'home' ? (
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone === 5 ? '71' : ''
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
) : (
<Link href={`/home/${currentWorkspace?._id}`}>
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
<p className="flex w-10 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone === 5 ? '71' : ''
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
</Link>
)}
</div>
</nav>
</aside>
<Modal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle('addNewWs', isModalOpen);
reset();
}}
>
<ModalContent
title="Create a new project"
subTitle="This project will contain your secrets and configurations."
>
<form onSubmit={handleSubmit(onCreateProject)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<div className="pl-1 mt-4">
<Controller
control={control}
name="addMembers"
defaultValue
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
)}
/>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose('addNewWs')}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
{children}
</main>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t('common:no-mobile')} `}
</p>
</div>
</>
);
};

View File

@ -0,0 +1,305 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/jsx-key */
import { Fragment, useMemo } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { TFunction, useTranslation } from 'next-i18next';
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import {
faAngleDown,
faBook,
faCoins,
faEnvelope,
faGear,
faPlus,
faRightFromBracket
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import guidGenerator from '@app/components/utilities/randomId';
import { useOrganization, useUser } from '@app/context';
import { useLogoutUser } from '@app/hooks/api';
const supportOptions = (t: TFunction) => [
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faSlack} />,
t('nav:support.slack'),
'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faBook} />,
t('nav:support.docs'),
'https://infisical.com/docs/getting-started/introduction'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faGithub} />,
t('nav:support.issue'),
'https://github.com/Infisical/infisical-cli/issues'
],
[
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faEnvelope} />,
t('nav:support.email'),
'mailto:support@infisical.com'
]
];
export interface ICurrentOrg {
name: string;
}
export interface IUser {
firstName: string;
lastName: string;
email: string;
}
/**
* This is the navigation bar in the main app.
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
* @returns NavBar
*/
export const Navbar = () => {
const router = useRouter();
const { currentOrg, orgs } = useOrganization();
const { user } = useUser();
const logout = useLogoutUser();
const { t } = useTranslation();
// remove this memo
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
const closeApp = async () => {
try {
console.log('Logging out...');
await logout.mutateAsync();
router.push('/login');
} catch (error) {
console.error(error);
}
};
return (
<div className="z-[70] flex w-full flex-row justify-between border-b border-mineshaft-500 bg-mineshaft-900 text-white">
<div className="m-auto mx-4 flex items-center justify-start">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
</div>
<a href="#" className="mx-2 text-2xl font-semibold text-white">
Infisical
</a>
</div>
</div>
<div className="relative z-40 mx-2 flex items-center justify-start">
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mr-4 flex items-center rounded-md px-3 py-2 text-sm text-gray-200 duration-200 hover:bg-white/10"
>
<FontAwesomeIcon icon={faBook} className="mr-2 text-xl" />
Docs
</a>
<Menu as="div" className="relative inline-block text-left">
<div className="mr-4">
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-20 mt-0.5 w-64 origin-top-right rounded-md border border-mineshaft-700 bg-bunker px-2 py-1.5 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{supportOptionsList.map(([icon, text, url]) => (
<a
key={guidGenerator()}
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md py-0.5 font-normal text-gray-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
))}
</Menu.Items>
</Transition>
</Menu>
<Menu as="div" className="relative mr-4 inline-block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-center rounded-md py-2 pr-2 pl-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
{user?.firstName} {user?.lastName}
<FontAwesomeIcon
icon={faAngleDown}
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-[125] mt-0.5 w-68 origin-top-right divide-y divide-mineshaft-700 drop-shadow-2xl rounded-md border border-mineshaft-700 bg-mineshaft-900 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="px-1 py-1">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.signed-in-as')}
</div>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
>
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
{user?.firstName?.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<div>
<p className="px-2 pt-1 text-sm text-gray-300">
{' '}
{user?.firstName} {user?.lastName}
</p>
<p className="px-2 pb-1 text-xs text-gray-400"> {user?.email}</p>
</div>
<FontAwesomeIcon
icon={faGear}
className="mr-1 cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
/>
</div>
</div>
</div>
<div className="px-2 pt-2">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.current-organization')}
</div>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/org/${router.query.id}`)}
className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5"
>
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
{currentOrg?.name?.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<p className="px-2 text-sm text-gray-300">{currentOrg?.name}</p>
<FontAwesomeIcon
icon={faGear}
className="cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
/>
</div>
</div>
<button
// onClick={buttonAction}
type="button"
className="w-full cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
>
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} />
<div className="text-sm">{t('nav:user.usage-billing')}</div>
</div>
</button>
<button
type="button"
// onClick={buttonAction}
className="mb-2 w-full cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-primary/100 hover:font-semibold hover:text-black"
>
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="ml-1" />
</span>
<div className="ml-1 text-sm">{t('nav:user.invite')}</div>
</div>
</button>
</div>
{orgs && orgs?.length > 1 && (
<div className="px-1 pt-1">
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:user.other-organizations')}
</div>
<div className="mt-3 mb-2 flex flex-col items-start px-1">
{orgs
?.filter((org: { _id: string }) => org._id !== currentOrg?._id)
.map((org: { _id: string; name: string }) => (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
key={guidGenerator()}
onClick={() => {
localStorage.setItem('orgData.id', org._id);
router.reload();
}}
className="flex w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5"
>
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
{org.name.charAt(0)}
</div>
<div className="flex w-full items-center justify-between">
<p className="px-2 text-sm text-gray-300">{org.name}</p>
</div>
</div>
))}
</div>
</div>
)}
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
type="button"
onClick={closeApp}
className={`${
active ? 'bg-red font-semibold text-white' : 'text-gray-400'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<div className="relative flex cursor-pointer select-none items-center justify-start">
<FontAwesomeIcon
className="ml-1.5 mr-3 text-lg"
icon={faRightFromBracket}
/>
{t('common:logout')}
</div>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { Navbar } from './NavBar';

View File

@ -0,0 +1 @@
export { AppLayout } from './AppLayout';

Some files were not shown because too many files have changed in this diff Show More