mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-24 21:44:53 +00:00
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
da857f321b | |||
c7dd028771 | |||
3c94bacda9 | |||
8e85847de3 | |||
0c10bbb569 | |||
fba54ae0c6 | |||
e243c72ca6 | |||
23ea6fd4f9 | |||
3f9f2ef238 | |||
77cb20f5c7 | |||
ddf630c269 | |||
39adb9a0c2 | |||
97fde96b7b | |||
190391e493 | |||
6f6df3e63a | |||
23c740d225 | |||
702d4de3b5 | |||
445fa35ab5 | |||
9868476965 | |||
bfa6b955ca | |||
90f5934440 | |||
0adc3d2027 | |||
edf0294d51 | |||
8850b44115 | |||
17f9e53779 | |||
a61233d2ba | |||
2022988e77 | |||
409de81bd2 | |||
2b289ddf77 | |||
b066a55ead | |||
8dfc0138f5 | |||
517f508e44 | |||
2f1a671121 | |||
2fb4b261a8 | |||
9c3c745fdf | |||
6a75147719 | |||
295b363d8a | |||
d96b5943b9 | |||
8fd2578a6d | |||
cc809a6bc0 | |||
66659c8fc8 | |||
31293bbe06 | |||
1c3488f8db | |||
20e536cec0 | |||
e8b498ca6d | |||
b5bcd0a308 | |||
03c72ea00f | |||
a486390015 | |||
8dc47110a0 | |||
52a6fe64a7 | |||
e6539a5566 | |||
07c056523f | |||
80d219c3e0 | |||
b0ffac2f00 | |||
5ba851adff |
12
.github/workflows/release_build.yml
vendored
12
.github/workflows/release_build.yml
vendored
@ -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 }}
|
||||
|
||||
|
@ -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"
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
@ -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 }
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
|
@ -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
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -109,6 +109,9 @@ const secretSchema = new Schema<ISecret>(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
secretSchema.index({ tags: 1 }, { background: true })
|
||||
|
||||
const Secret = model<ISecret>('Secret', secretSchema);
|
||||
|
||||
export default Secret;
|
||||
|
83
backend/src/models/secretApprovalRequest.ts
Normal file
83
backend/src/models/secretApprovalRequest.ts
Normal 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;
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -74,6 +74,7 @@ router.get(
|
||||
'/',
|
||||
query('workspaceId').exists().trim(),
|
||||
query('environment').exists().trim(),
|
||||
query('tagSlugs'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
4
cli/docker/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
||||
FROM alpine
|
||||
RUN apk add --no-cache tini
|
||||
COPY infisical /bin/infisical
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"]
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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]
|
||||
|
11
docs/cli/commands/reset.mdx
Normal file
11
docs/cli/commands/reset.mdx
Normal 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.
|
@ -114,7 +114,8 @@
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/export",
|
||||
"cli/commands/vault"
|
||||
"cli/commands/vault",
|
||||
"cli/commands/reset"
|
||||
]
|
||||
},
|
||||
"cli/faq"
|
||||
|
@ -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` |
|
||||
|
@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
|
||||
'netlify': 'Netlify',
|
||||
'github': 'GitHub',
|
||||
'render': 'Render',
|
||||
'flyio': 'Fly.io'
|
||||
'flyio': 'Fly.io',
|
||||
"circleci": 'CircleCI'
|
||||
}
|
||||
|
||||
const envMapping: Mapping = {
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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'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" &&
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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" />
|
||||
|
@ -76,7 +76,7 @@ const encryptSecrets = async ({
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.value,
|
||||
plaintext: secret.value ?? '',
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
|
49
frontend/src/components/v2/Popover/Popover.tsx
Normal file
49
frontend/src/components/v2/Popover/Popover.tsx
Normal 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';
|
2
frontend/src/components/v2/Popover/index.tsx
Normal file
2
frontend/src/components/v2/Popover/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export type { PopoverProps } from './Popover';
|
||||
export { PopoverObject } from './Popover';
|
@ -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>
|
||||
|
@ -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?';
|
||||
|
@ -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;
|
||||
};
|
1
frontend/src/context/OrganizationContext/index.tsx
Normal file
1
frontend/src/context/OrganizationContext/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { OrgProvider, useOrganization } from './OrganizationContext';
|
38
frontend/src/context/UserContext/UserContext.tsx
Normal file
38
frontend/src/context/UserContext/UserContext.tsx
Normal 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;
|
||||
};
|
1
frontend/src/context/UserContext/index.tsx
Normal file
1
frontend/src/context/UserContext/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { UserProvider, useUser } from './UserContext';
|
@ -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]);
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -1 +1 @@
|
||||
export { useGetUserWsKey } from './queries';
|
||||
export { useGetUserWsKey, useUploadWsKey } from './queries';
|
||||
|
@ -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 } })
|
||||
});
|
||||
|
@ -20,3 +20,10 @@ export type Sender = {
|
||||
lastName: string;
|
||||
publicKey: string;
|
||||
};
|
||||
|
||||
export type UploadWsKeyDTO = {
|
||||
userId: string;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
1
frontend/src/hooks/api/organization/index.ts
Normal file
1
frontend/src/hooks/api/organization/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { useGetOrganization } from './queries';
|
18
frontend/src/hooks/api/organization/queries.tsx
Normal file
18
frontend/src/hooks/api/organization/queries.tsx
Normal 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 });
|
6
frontend/src/hooks/api/organization/types.ts
Normal file
6
frontend/src/hooks/api/organization/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type Organization = {
|
||||
_id: string;
|
||||
name: string;
|
||||
createAt: string;
|
||||
updatedAt: string;
|
||||
};
|
@ -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,
|
||||
|
7
frontend/src/hooks/api/users/index.tsx
Normal file
7
frontend/src/hooks/api/users/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWs,
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useLogoutUser
|
||||
} from './queries';
|
84
frontend/src/hooks/api/users/queries.tsx
Normal file
84
frontend/src/hooks/api/users/queries.tsx
Normal 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', '');
|
||||
}
|
||||
});
|
39
frontend/src/hooks/api/users/types.ts
Normal file
39
frontend/src/hooks/api/users/types.ts
Normal 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;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
export {
|
||||
useCreateWorkspace,
|
||||
useCreateWsEnvironment,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { usePopUp } from './usePopUp';
|
||||
export { useToggle } from './useToggle';
|
||||
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||
|
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal file
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal 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,
|
||||
};
|
||||
}
|
455
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal file
455
frontend/src/layouts/AppLayout/AppLayout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
305
frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
Normal file
305
frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { Navbar } from './NavBar';
|
1
frontend/src/layouts/AppLayout/index.tsx
Normal file
1
frontend/src/layouts/AppLayout/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { AppLayout } from './AppLayout';
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user