mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 19:26:07 +00:00
Merge remote-tracking branch 'origin' into mfa
This commit is contained in:
+4
-2
@@ -2,7 +2,8 @@ frontend:
|
|||||||
enabled: true
|
enabled: true
|
||||||
name: frontend
|
name: frontend
|
||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
deploymentAnnotations: {}
|
deploymentAnnotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
image:
|
image:
|
||||||
repository: infisical/frontend
|
repository: infisical/frontend
|
||||||
@@ -20,7 +21,8 @@ backend:
|
|||||||
enabled: true
|
enabled: true
|
||||||
name: backend
|
name: backend
|
||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
deploymentAnnotations: {}
|
deploymentAnnotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
image:
|
image:
|
||||||
repository: infisical/backend
|
repository: infisical/backend
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
||||||
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
|
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
|
||||||
- **Role-based Access Controls** per environment
|
- **Role-based Access Controls** per environment
|
||||||
- 🔜 **2FA** (next week)
|
- **2FA** (more options coming soon)
|
||||||
- 🔜 **1-Click Deploy** to AWS
|
- 🔜 **1-Click Deploy** to AWS
|
||||||
- 🔜 **Automatic Secret Rotation**
|
- 🔜 **Automatic Secret Rotation**
|
||||||
- 🔜 **Smart Security Alerts**
|
- 🔜 **Smart Security Alerts**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User } from '../../models';
|
import { User, LoginSRPDetail } from '../../models';
|
||||||
import { issueAuthTokens, createToken } from '../../helpers/auth';
|
import { issueAuthTokens, createToken } from '../../helpers/auth';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { TokenService } from '../../services';
|
import { TokenService } from '../../services';
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
JWT_MFA_LIFETIME,
|
JWT_MFA_LIFETIME,
|
||||||
JWT_MFA_SECRET
|
JWT_MFA_SECRET
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import {
|
import {
|
||||||
TOKEN_EMAIL_MFA
|
TOKEN_EMAIL_MFA
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
@@ -50,13 +51,15 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
() => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
clientPublicKeys[email] = {
|
|
||||||
clientPublicKey,
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
email: email,
|
||||||
};
|
clientPublicKey: clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
|
}, { upsert: true, returnNewDocument: false });
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
@@ -89,15 +92,21 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
|
if (!loginSRPDetail) {
|
||||||
|
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||||
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: clientPublicKeys[email].serverBInt
|
b: loginSRPDetail.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const exchangeCodeAzure = async ({
|
|||||||
try {
|
try {
|
||||||
res = (await axios.post(
|
res = (await axios.post(
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: code,
|
code: code,
|
||||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
scope: 'https://vault.azure.net/.default openid offline_access',
|
||||||
@@ -147,16 +147,16 @@ const exchangeCodeAzure = async ({
|
|||||||
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||||
} as any)
|
} as any)
|
||||||
)).data;
|
)).data;
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(
|
||||||
accessExpiresAt.getSeconds() + res.expires_in
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
@@ -175,36 +175,36 @@ const exchangeCodeAzure = async ({
|
|||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeHeroku = async ({
|
const exchangeCodeHeroku = async ({
|
||||||
code
|
code
|
||||||
}: {
|
}: {
|
||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
let res: ExchangeCodeHerokuResponse;
|
let res: ExchangeCodeHerokuResponse;
|
||||||
const accessExpiresAt = new Date();
|
const accessExpiresAt = new Date();
|
||||||
try {
|
try {
|
||||||
res = (await axios.post(
|
res = (await axios.post(
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: code,
|
code: code,
|
||||||
client_secret: CLIENT_SECRET_HEROKU
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
} as any)
|
} as any)
|
||||||
)).data;
|
)).data;
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(
|
||||||
accessExpiresAt.getSeconds() + res.expires_in
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,7 +234,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_CIRCLECI_API_URL,
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import { access, appendFile } from "fs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to [app] in integration named [integration]
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
@@ -181,7 +180,8 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
while (url) {
|
while (url) {
|
||||||
const res = await axios.get(url, {
|
const res = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,7 +202,8 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
|
|
||||||
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,7 +260,8 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -270,7 +272,8 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
deleteSecrets.forEach(async (secret) => {
|
deleteSecrets.forEach(async (secret) => {
|
||||||
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -488,6 +491,7 @@ const syncSecretsHeroku = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.heroku+json; version=3",
|
Accept: "application/vnd.heroku+json; version=3",
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -506,6 +510,7 @@ const syncSecretsHeroku = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.heroku+json; version=3",
|
Accept: "application/vnd.heroku+json; version=3",
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -552,7 +557,7 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = (
|
const res = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(
|
(
|
||||||
@@ -561,7 +566,8 @@ const syncSecretsVercel = async ({
|
|||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -573,7 +579,8 @@ const syncSecretsVercel = async ({
|
|||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)).data)
|
)).data)
|
||||||
@@ -633,6 +640,7 @@ const syncSecretsVercel = async ({
|
|||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -649,6 +657,7 @@ const syncSecretsVercel = async ({
|
|||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -664,6 +673,7 @@ const syncSecretsVercel = async ({
|
|||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -723,6 +733,7 @@ const syncSecretsNetlify = async ({
|
|||||||
params: getParams,
|
params: getParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -837,6 +848,7 @@ const syncSecretsNetlify = async ({
|
|||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -854,6 +866,7 @@ const syncSecretsNetlify = async ({
|
|||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -868,6 +881,7 @@ const syncSecretsNetlify = async ({
|
|||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -882,6 +896,7 @@ const syncSecretsNetlify = async ({
|
|||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1035,6 +1050,7 @@ const syncSecretsRender = async ({
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1088,6 +1104,7 @@ const syncSecretsFlyio = async ({
|
|||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: "Bearer " + accessToken,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
query: SetSecrets,
|
query: SetSecrets,
|
||||||
@@ -1167,6 +1184,7 @@ const syncSecretsFlyio = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: "Bearer " + accessToken,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
query: DeleteSecrets,
|
query: DeleteSecrets,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface SecretDataProps {
|
|||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
valueOverride: string | undefined;
|
valueOverride: string | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
|
idOverride?: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ type Props = {
|
|||||||
type EnvironmentProps = {
|
type EnvironmentProps = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the component that shows the users of a certin project
|
* This is the component that shows the users of a certin project
|
||||||
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
|
const handlePermissionUpdate = (
|
||||||
let denials: { ability: string; environmentSlug: string; }[];
|
index: number,
|
||||||
if (val === "Read Only") {
|
val: string,
|
||||||
denials = [{
|
membershipId: string,
|
||||||
ability: "write",
|
slug: string
|
||||||
environmentSlug: slug
|
) => {
|
||||||
}];
|
let denials: { ability: string; environmentSlug: string }[];
|
||||||
} else if (val === "No Access") {
|
if (val === 'Read Only') {
|
||||||
denials = [{
|
denials = [
|
||||||
ability: "write",
|
{
|
||||||
environmentSlug: slug
|
ability: 'write',
|
||||||
}, {
|
environmentSlug: slug
|
||||||
ability: "read",
|
}
|
||||||
environmentSlug: slug
|
];
|
||||||
}];
|
} else if (val === 'No Access') {
|
||||||
} else if (val === "Add Only") {
|
denials = [
|
||||||
denials = [{
|
{
|
||||||
ability: "read",
|
ability: 'write',
|
||||||
environmentSlug: slug
|
environmentSlug: slug
|
||||||
}];
|
},
|
||||||
|
{
|
||||||
|
ability: 'read',
|
||||||
|
environmentSlug: slug
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else if (val === 'Add Only') {
|
||||||
|
denials = [
|
||||||
|
{
|
||||||
|
ability: 'read',
|
||||||
|
environmentSlug: slug
|
||||||
|
}
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
denials = [];
|
denials = [];
|
||||||
}
|
}
|
||||||
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
||||||
setIsUpgradeModalOpen(true);
|
setIsUpgradeModalOpen(true);
|
||||||
} else {
|
} else {
|
||||||
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
|
const allDenials = userData[index].deniedPermissions
|
||||||
updateUserProjectPermission({ membershipId, denials: allDenials});
|
.filter(
|
||||||
|
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
|
||||||
|
)
|
||||||
|
.concat(denials);
|
||||||
|
updateUserProjectPermission({ membershipId, denials: allDenials });
|
||||||
changeData([
|
changeData([
|
||||||
...userData.slice(0, index),
|
...userData.slice(0, index),
|
||||||
...[
|
...[
|
||||||
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
if (subscriptions) {
|
if (subscriptions) {
|
||||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
setCurrentPlan(subscriptions.data[0].plan.product);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [userData, myUser]);
|
}, [userData, myUser]);
|
||||||
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
|
|
||||||
const closeUpgradeModal = () => {
|
const closeUpgradeModal = () => {
|
||||||
setIsUpgradeModalOpen(false);
|
setIsUpgradeModalOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
|
||||||
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
|
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={isUpgradeModalOpen}
|
isOpen={isUpgradeModalOpen}
|
||||||
onClose={closeUpgradeModal}
|
onClose={closeUpgradeModal}
|
||||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||||
/>
|
/>
|
||||||
<table className="w-full my-0.5">
|
<table className="my-0.5 w-full">
|
||||||
<thead className="text-gray-400 text-xs font-light">
|
<thead className="text-xs font-light text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
<th className="py-3.5 pl-4 text-left">NAME</th>
|
||||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
<th className="py-3.5 pl-4 text-left">EMAIL</th>
|
||||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
|
||||||
{workspaceEnvs.map(env => (
|
{workspaceEnvs.map((env) => (
|
||||||
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
|
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
|
||||||
<span>{env.slug.toUpperCase()}<br/></span>
|
<span>
|
||||||
|
{env.slug.toUpperCase()}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
{/* <span>PERMISSION</span> */}
|
{/* <span>PERMISSION</span> */}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
)
|
)
|
||||||
.map((row, index) => (
|
.map((row, index) => (
|
||||||
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||||
{row.firstName} {row.lastName}
|
{row.firstName} {row.lastName}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||||
{row.email}
|
{row.email}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
|
||||||
<div className="justify-start h-full flex flex-row items-center">
|
<div className="flex h-full flex-row items-center justify-start">
|
||||||
<Select
|
<Select
|
||||||
className="w-36 bg-mineshaft-700"
|
className="w-36 bg-mineshaft-700"
|
||||||
dropdownContainerClassName="bg-mineshaft-700"
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
// open={isOpen}
|
// open={isOpen}
|
||||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||||
value={row.role}
|
value={row.role}
|
||||||
disabled={myRole !== 'admin' || myUser === row.email}
|
isDisabled={myRole !== 'admin' || myUser === row.email}
|
||||||
// onOpenChange={(open) => setIsOpen(open)}
|
// onOpenChange={(open) => setIsOpen(open)}
|
||||||
>
|
>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
{row.status === 'completed' && myUser !== row.email && (
|
{row.status === 'completed' && myUser !== row.email && (
|
||||||
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
|
{workspaceEnvs.map((env) => (
|
||||||
<Select
|
<td
|
||||||
className="w-16 bg-mineshaft-700"
|
key={guidGenerator()}
|
||||||
dropdownContainerClassName="bg-mineshaft-700"
|
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
|
||||||
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"
|
|
||||||
// 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" customIcon={faEyeSlash}>No Access</SelectItem>
|
<Select
|
||||||
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
|
className="w-16 bg-mineshaft-700"
|
||||||
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
|
position="item-aligned"
|
||||||
</Select>
|
// open={isOpen}
|
||||||
</td>)}
|
onValueChange={(val) =>
|
||||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
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'
|
||||||
|
: // 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
|
||||||
|
}
|
||||||
|
isDisabled={myRole !== 'admin'}
|
||||||
|
// onOpenChange={(open) => setIsOpen(open)}
|
||||||
|
>
|
||||||
|
<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="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
|
||||||
{myUser !== row.email &&
|
{myUser !== row.email &&
|
||||||
// row.role !== "admin" &&
|
// row.role !== "admin" &&
|
||||||
myRole !== 'member' ? (
|
myRole !== 'member' ? (
|
||||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||||
color="red"
|
color="red"
|
||||||
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-9 h-9" />
|
<div className="h-9 w-9" />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ const DashboardInputField = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
|
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
|
||||||
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
|
overrideEnabled ? 'visible' : 'invisible group-hover:visible'
|
||||||
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
||||||
<button type="button" onClick={() => {
|
<button type="button" onClick={() => {
|
||||||
if (modifyValueOverride) {
|
if (modifyValueOverride) {
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const DropZone = ({
|
|||||||
id="fileSelect"
|
id="fileSelect"
|
||||||
type="file"
|
type="file"
|
||||||
className="opacity-0 absolute w-full h-full"
|
className="opacity-0 absolute w-full h-full"
|
||||||
accept=".txt,.env,.yml"
|
accept=""
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const colorsText = [
|
|||||||
/**
|
/**
|
||||||
* This component represent a single row for an environemnt variable on the dashboard
|
* This component represent a single row for an environemnt variable on the dashboard
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
* @param {SecretDataProps[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||||
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
||||||
@@ -108,7 +108,7 @@ const KeyPair = ({
|
|||||||
isCapitalized = {isCapitalized}
|
isCapitalized = {isCapitalized}
|
||||||
onChangeHandler={modifyKey}
|
onChangeHandler={modifyKey}
|
||||||
type="varName"
|
type="varName"
|
||||||
id={keyPair.id}
|
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||||
value={keyPair.key}
|
value={keyPair.key}
|
||||||
isDuplicate={isDuplicate}
|
isDuplicate={isDuplicate}
|
||||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||||
@@ -124,7 +124,7 @@ const KeyPair = ({
|
|||||||
<DashboardInputField
|
<DashboardInputField
|
||||||
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
|
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
|
||||||
type="value"
|
type="value"
|
||||||
id={keyPair.id}
|
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||||
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
|
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
|
||||||
blurred={isBlurred}
|
blurred={isBlurred}
|
||||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||||
@@ -137,7 +137,7 @@ const KeyPair = ({
|
|||||||
<DashboardInputField
|
<DashboardInputField
|
||||||
onChangeHandler={modifyComment}
|
onChangeHandler={modifyComment}
|
||||||
type="comment"
|
type="comment"
|
||||||
id={keyPair.id}
|
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||||
value={keyPair.comment}
|
value={keyPair.comment}
|
||||||
isDuplicate={isDuplicate}
|
isDuplicate={isDuplicate}
|
||||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||||
@@ -153,7 +153,7 @@ const KeyPair = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} id={keyPair.id} />
|
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function MFAStep({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
const [mfaCode, setMfaCode] = useState('');
|
const [mfaCode, setMfaCode] = useState('');
|
||||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
@@ -103,9 +104,12 @@ export default function MFAStep({
|
|||||||
|
|
||||||
const handleResendMfaCode = async () => {
|
const handleResendMfaCode = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsLoadingResend(true);
|
||||||
await sendMfaToken.mutateAsync({ email });
|
await sendMfaToken.mutateAsync({ email });
|
||||||
|
setIsLoadingResend(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setIsLoadingResend(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +141,13 @@ export default function MFAStep({
|
|||||||
<span className="text-bunker-400">{t('mfa:step2-resend-alert')}</span>
|
<span className="text-bunker-400">{t('mfa:step2-resend-alert')}</span>
|
||||||
<u
|
<u
|
||||||
className={`font-normal ${
|
className={`font-normal ${
|
||||||
isLoading
|
isLoadingResend
|
||||||
? 'text-bunker-400'
|
? 'text-bunker-400'
|
||||||
: 'text-primary-700 hover:text-primary duration-200'
|
: 'text-primary-700 hover:text-primary duration-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
|
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
|
||||||
{isLoading
|
{isLoadingResend
|
||||||
? t('mfa:step2-resend-progress')
|
? t('mfa:step2-resend-progress')
|
||||||
: t('mfa:step2-resend-submit')}
|
: t('mfa:step2-resend-submit')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function Navbar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[61]">
|
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[71]">
|
||||||
<div className="m-auto flex justify-start items-center mx-4">
|
<div className="m-auto flex justify-start items-center mx-4">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="flex justify-center py-4">
|
<div className="flex justify-center py-4">
|
||||||
@@ -165,7 +165,7 @@ export default function Navbar() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[65] ring-opacity-5 focus:outline-none">
|
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
|
||||||
<div className="px-1 py-1 z-[100]">
|
<div className="px-1 py-1 z-[100]">
|
||||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||||
{t('nav:user.signed-in-as')}
|
{t('nav:user.signed-in-as')}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import { deriveArgonKey } from './crypto';
|
|||||||
const clientOldPassword = new jsrp.client();
|
const clientOldPassword = new jsrp.client();
|
||||||
const clientNewPassword = new jsrp.client();
|
const clientNewPassword = new jsrp.client();
|
||||||
|
|
||||||
// TODO: modify this function
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||||
* @param {*} email
|
* @param {*} email
|
||||||
@@ -105,9 +103,8 @@ const changePassword = async (
|
|||||||
secret: Buffer.from(derivedKey.hash)
|
secret: Buffer.from(derivedKey.hash)
|
||||||
});
|
});
|
||||||
|
|
||||||
let res;
|
|
||||||
try {
|
try {
|
||||||
res = await changePassword2({
|
await changePassword2({
|
||||||
clientProof,
|
clientProof,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
@@ -118,23 +115,19 @@ const changePassword = async (
|
|||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier
|
verifier: result.verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
saveTokenToLocalStorage({
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.status === 400) {
|
setPasswordChanged(true);
|
||||||
setCurrentPasswordError(true);
|
setCurrentPassword('');
|
||||||
} else if (res && res.status === 200) {
|
setNewPassword('');
|
||||||
setPasswordChanged(true);
|
|
||||||
setCurrentPassword('');
|
|
||||||
setNewPassword('');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCurrentPasswordError(true);
|
setCurrentPasswordError(true);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ interface Props {
|
|||||||
protectedKeyIV?: string;
|
protectedKeyIV?: string;
|
||||||
protectedKeyTag?: string;
|
protectedKeyTag?: string;
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey?: string;
|
||||||
iv: string;
|
iv?: string;
|
||||||
tag: string;
|
tag?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,38 +20,46 @@ export const saveTokenToLocalStorage = ({
|
|||||||
privateKey,
|
privateKey,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem("protectedKey");
|
|
||||||
localStorage.removeItem("protectedKeyIV");
|
|
||||||
localStorage.removeItem("protectedKeyTag");
|
|
||||||
localStorage.removeItem("publicKey");
|
|
||||||
localStorage.removeItem("encryptedPrivateKey");
|
|
||||||
localStorage.removeItem("iv");
|
|
||||||
localStorage.removeItem("tag");
|
|
||||||
localStorage.removeItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (protectedKey) {
|
if (protectedKey) {
|
||||||
|
localStorage.removeItem("protectedKey");
|
||||||
localStorage.setItem("protectedKey", protectedKey);
|
localStorage.setItem("protectedKey", protectedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectedKeyIV) {
|
if (protectedKeyIV) {
|
||||||
|
localStorage.removeItem("protectedKeyIV");
|
||||||
localStorage.setItem("protectedKeyIV", protectedKeyIV);
|
localStorage.setItem("protectedKeyIV", protectedKeyIV);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectedKeyTag) {
|
if (protectedKeyTag) {
|
||||||
|
localStorage.removeItem("protectedKeyTag");
|
||||||
localStorage.setItem("protectedKeyTag", protectedKeyTag);
|
localStorage.setItem("protectedKeyTag", protectedKeyTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicKey) {
|
if (publicKey) {
|
||||||
|
localStorage.removeItem("publicKey");
|
||||||
localStorage.setItem("publicKey", publicKey);
|
localStorage.setItem("publicKey", publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKey) {
|
if (encryptedPrivateKey) {
|
||||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
localStorage.removeItem("encryptedPrivateKey");
|
||||||
|
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
if (iv) {
|
||||||
localStorage.setItem("iv", iv);
|
localStorage.removeItem("iv");
|
||||||
localStorage.setItem("tag", tag);
|
localStorage.setItem("iv", iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
localStorage.removeItem("tag");
|
||||||
|
localStorage.setItem("tag", tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey) {
|
||||||
|
localStorage.removeItem("PRIVATE_KEY");
|
||||||
|
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
export type CardTitleProps = {
|
export type CardTitleProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
subTitle?: string;
|
subTitle?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||||
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
|
{subTitle && <p className="pt-2 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
|
|||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { FormControl } from '../FormControl';
|
import { FormControl } from '../FormControl';
|
||||||
import { Input } from '../Input';
|
import { Input } from '../Input';
|
||||||
import { Modal, ModalContent } from '../Modal';
|
import { Modal, ModalClose, ModalContent } from '../Modal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
<ModalClose asChild>
|
||||||
Cancel
|
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||||
</Button>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>{' '}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
|
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
@@ -84,19 +84,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
||||||
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
|
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
leftIcon ? 'pl-9' : 'pl-2.5',
|
leftIcon ? 'pl-10' : 'pl-2.5',
|
||||||
rightIcon ? 'pr-9' : 'pr-2.5',
|
rightIcon ? 'pr-10' : 'pr-2.5',
|
||||||
inputVariants({ className, isError, size, isRounded, variant })
|
inputVariants({ className, isError, size, isRounded, variant })
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
|
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
|
|||||||
|
|
||||||
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
subTitle?: string;
|
subTitle?: ReactNode;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,18 +14,28 @@ type Props = {
|
|||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: 'item-aligned' | 'popper';
|
position?: 'item-aligned' | 'popper';
|
||||||
|
isDisabled?: boolean;
|
||||||
icon?: IconProp;
|
icon?: IconProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
export type SelectProps = Omit<SelectPrimitive.SelectProps, 'disabled'> & Props;
|
||||||
|
|
||||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
(
|
(
|
||||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
|
{
|
||||||
|
children,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
isLoading,
|
||||||
|
isDisabled,
|
||||||
|
dropdownContainerClassName,
|
||||||
|
position,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Root {...props}>
|
<SelectPrimitive.Root {...props} disabled={isDisabled}>
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -34,10 +44,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Value placeholder={placeholder}>
|
<SelectPrimitive.Value placeholder={placeholder}>
|
||||||
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||||
</SelectPrimitive.Value>
|
</SelectPrimitive.Value>
|
||||||
{!props.disabled && (
|
{!isDisabled && (
|
||||||
<SelectPrimitive.Icon className="ml-3">
|
<SelectPrimitive.Icon className="ml-3">
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
@@ -46,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
'relative top-1 z-[100] overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -89,11 +99,12 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative flex cursor-pointer
|
`relative mb-0.5 flex
|
||||||
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||||
outline-none transition-all hover:bg-mineshaft-500`,
|
outline-none transition-all hover:bg-mineshaft-500`,
|
||||||
isSelected && 'bg-primary',
|
isSelected && 'bg-primary',
|
||||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
isDisabled &&
|
||||||
|
'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { cva, VariantProps } from 'cva';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & VariantProps<typeof tagVariants>;
|
||||||
|
|
||||||
|
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 ', {
|
||||||
|
variants: {
|
||||||
|
colorSchema: {
|
||||||
|
gray: 'bg-mineshaft-500',
|
||||||
|
red: 'bg-red/80 text-bunker-100'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: 'px-1 py-0.5'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
|
||||||
|
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Tag } from './Tag';
|
||||||
@@ -12,5 +12,6 @@ export * from './Select';
|
|||||||
export * from './Spinner';
|
export * from './Spinner';
|
||||||
export * from './Switch';
|
export * from './Switch';
|
||||||
export * from './Table';
|
export * from './Table';
|
||||||
|
export * from './Tag';
|
||||||
export * from './TextArea';
|
export * from './TextArea';
|
||||||
export * from './UpgradePlanModal';
|
export * from './UpgradePlanModal';
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map(
|
const decryptedSecretVersions = secretSnapshotData.secretVersions.filter(
|
||||||
|
(sv: EncrypetedSecretVersionListProps) => (sv.type !== undefined && sv.environment !== undefined)
|
||||||
|
).map(
|
||||||
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||||
id: encryptedSecretVersion._id,
|
id: encryptedSecretVersion._id,
|
||||||
pos,
|
pos,
|
||||||
@@ -125,10 +127,13 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const secretKeys = [
|
const secretKeys = [
|
||||||
...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))
|
...new Set(decryptedSecretVersions.filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined)
|
||||||
|
.map((secret: SecretDataProps) => secret.key))
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = secretKeys.map((key, index) => ({
|
const result = secretKeys.map((key, index) => (decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
|
)[0]?.id ? {
|
||||||
id: decryptedSecretVersions.filter(
|
id: decryptedSecretVersions.filter(
|
||||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
)[0].id,
|
)[0].id,
|
||||||
@@ -146,6 +151,24 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
valueOverride: decryptedSecretVersions.filter(
|
valueOverride: decryptedSecretVersions.filter(
|
||||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
)[0]?.value
|
)[0]?.value
|
||||||
|
} : {
|
||||||
|
id: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0].id,
|
||||||
|
pos: index,
|
||||||
|
key,
|
||||||
|
environment: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0].environment,
|
||||||
|
tags: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0].tags,
|
||||||
|
value: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
|
)[0]?.value,
|
||||||
|
valueOverride: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0]?.value
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setSnapshotData({
|
setSnapshotData({
|
||||||
@@ -161,7 +184,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
|||||||
<div
|
<div
|
||||||
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
|
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
|
||||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
} fixed h-full right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
|
} fixed h-full right-0 z-[40] shadow-xl flex flex-col justify-between sticky top-0`}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
useAddIncidentContact,
|
||||||
|
useDeleteIncidentContact,
|
||||||
|
useGetOrgIncidentContact
|
||||||
|
} from './queries';
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
|
import { AddIncidentContactDTO, DeleteIncidentContactDTO, IncidentContact } from './types';
|
||||||
|
|
||||||
|
const incidentContactKeys = {
|
||||||
|
getAllContact: (orgId: string) => ['org-incident-contacts', { orgId }] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOrgIncidentContacts = async (orgId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
|
||||||
|
`/api/v1/organization/${orgId}/incidentContactOrg`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.incidentContactsOrg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetOrgIncidentContact = (orgId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: incidentContactKeys.getAllContact(orgId),
|
||||||
|
queryFn: () => fetchOrgIncidentContacts(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
export const useAddIncidentContact = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, AddIncidentContactDTO>({
|
||||||
|
mutationFn: async ({ orgId, email }) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/organization/${orgId}/incidentContactOrg`, {
|
||||||
|
email
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteIncidentContact = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, DeleteIncidentContactDTO>({
|
||||||
|
mutationFn: async ({ orgId, email }) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
|
||||||
|
data: { email }
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type IncidentContact = {
|
||||||
|
_id: string;
|
||||||
|
email: string;
|
||||||
|
organization: string;
|
||||||
|
__v: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteIncidentContactDTO = {
|
||||||
|
orgId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddIncidentContactDTO = {
|
||||||
|
orgId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './incidentContacts';
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
export * from './organization';
|
export * from './organization';
|
||||||
export * from './serviceTokens';
|
export * from './serviceTokens';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { useGetOrganization } from './queries';
|
export { useGetOrganization, useRenameOrg } from './queries';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { apiRequest } from '@app/config/request';
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
import { Organization } from './types';
|
import { Organization, RenameOrgDTO } from './types';
|
||||||
|
|
||||||
const organizationKeys = {
|
const organizationKeys = {
|
||||||
getUserOrganization: ['organization'] as const
|
getUserOrganization: ['organization'] as const
|
||||||
@@ -16,3 +16,16 @@ const fetchUserOrganization = async () => {
|
|||||||
|
|
||||||
export const useGetOrganization = () =>
|
export const useGetOrganization = () =>
|
||||||
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
|
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
export const useRenameOrg = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, RenameOrgDTO>({
|
||||||
|
mutationFn: ({ newOrgName, orgId }) =>
|
||||||
|
apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getUserOrganization);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ export type Organization = {
|
|||||||
createAt: string;
|
createAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RenameOrgDTO = {
|
||||||
|
orgId: string;
|
||||||
|
newOrgName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
export type { GetAuthTokenAPI } from './auth/types';
|
export type { GetAuthTokenAPI } from './auth/types';
|
||||||
|
export type { IncidentContact } from './incidentContacts/types';
|
||||||
export type { UserWsKeyPair } from './keys/types';
|
export type { UserWsKeyPair } from './keys/types';
|
||||||
export type { Organization } from './organization/types';
|
export type { Organization } from './organization/types';
|
||||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||||
export type { User } from './users/types';
|
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
|
||||||
export type {
|
export type {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
|
ToggleAutoCapitalizationDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceEnv,
|
WorkspaceEnv,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export {
|
export {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
|
useAddUserToOrg,
|
||||||
useAddUserToWs,
|
useAddUserToWs,
|
||||||
|
useDeleteOrgMembership,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useLogoutUser
|
useLogoutUser,
|
||||||
|
useUpdateOrgUserRole
|
||||||
} from './queries';
|
} from './queries';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@@ -8,7 +8,15 @@ import { apiRequest } from '@app/config/request';
|
|||||||
import { setAuthToken } from '@app/reactQuery';
|
import { setAuthToken } from '@app/reactQuery';
|
||||||
|
|
||||||
import { useUploadWsKey } from '../keys/queries';
|
import { useUploadWsKey } from '../keys/queries';
|
||||||
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
|
import {
|
||||||
|
AddUserToOrgDTO,
|
||||||
|
AddUserToWsDTO,
|
||||||
|
AddUserToWsRes,
|
||||||
|
DeletOrgMembershipDTO,
|
||||||
|
OrgUser,
|
||||||
|
UpdateOrgUserRoleDTO,
|
||||||
|
User
|
||||||
|
} from './types';
|
||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ['user'] as const,
|
getUser: ['user'] as const,
|
||||||
@@ -32,7 +40,11 @@ export const fetchOrgUsers = async (orgId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useGetOrgUsers = (orgId: string) =>
|
export const useGetOrgUsers = (orgId: string) =>
|
||||||
useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
|
useQuery({
|
||||||
|
queryKey: userKeys.getOrgUsers(orgId),
|
||||||
|
queryFn: () => fetchOrgUsers(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
export const useAddUserToWs = () => {
|
export const useAddUserToWs = () => {
|
||||||
@@ -69,6 +81,47 @@ export const useAddUserToWs = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddUserToOrg = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, AddUserToOrgDTO>({
|
||||||
|
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteOrgMembership = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, DeletOrgMembershipDTO>({
|
||||||
|
mutationFn: ({ membershipId, orgId }) =>
|
||||||
|
apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`),
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateOrgUserRole = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
|
||||||
|
mutationFn: ({ organizationId, membershipId, role }) =>
|
||||||
|
apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
||||||
|
role
|
||||||
|
}),
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
},
|
||||||
|
// to remove old states
|
||||||
|
onError: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useLogoutUser = () =>
|
export const useLogoutUser = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export type OrgUser = {
|
|||||||
inviteEmail: string;
|
inviteEmail: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: 'owner' | 'admin' | 'member';
|
role: 'owner' | 'admin' | 'member';
|
||||||
status: 'invited' | 'accepted';
|
status: 'invited' | 'accepted' | 'verified' | 'completed';
|
||||||
deniedPermissions: any[];
|
deniedPermissions: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,3 +45,19 @@ export type AddUserToWsRes = {
|
|||||||
invitee: OrgUser['user'];
|
invitee: OrgUser['user'];
|
||||||
latestKey: UserWsKeyPair;
|
latestKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateOrgUserRoleDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
membershipId: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletOrgMembershipDTO = {
|
||||||
|
membershipId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddUserToOrgDTO = {
|
||||||
|
inviteeEmail: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ export {
|
|||||||
useCreateWsEnvironment,
|
useCreateWsEnvironment,
|
||||||
useDeleteWorkspace,
|
useDeleteWorkspace,
|
||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateWsEnvironment} from './queries';
|
useUpdateWsEnvironment} from './queries';
|
||||||
|
|
||||||
@@ -13,16 +13,18 @@ import {
|
|||||||
Workspace
|
Workspace
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
|
||||||
const workspaceKeys = {
|
const workspaceKeys = {
|
||||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
||||||
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
|
||||||
getAllUserWorkspace: ['workspaces'] as const
|
getAllUserWorkspace: ['workspaces'] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
|
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
||||||
return data.workspace;
|
`/api/v1/workspace/${workspaceId}`
|
||||||
}
|
);
|
||||||
|
return data.workspace;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async () => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
|
||||||
@@ -40,6 +42,21 @@ export const useGetWorkspaceById = (workspaceId: string) => {
|
|||||||
export const useGetUserWorkspaces = () =>
|
export const useGetUserWorkspaces = () =>
|
||||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||||
|
|
||||||
|
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||||
|
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||||
|
`/api/v1/organization/${orgId}/workspace-memberships`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// to get all userids in an org with the workspace they are part of
|
||||||
|
export const useGetUserWorkspaceMemberships = (orgId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceMemberships(orgId),
|
||||||
|
queryFn: () => fetchUserWorkspaceMemberships(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -70,7 +87,9 @@ export const useToggleAutoCapitalization = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
|
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
|
||||||
mutationFn: ({ workspaceID, state }) =>
|
mutationFn: ({ workspaceID, state }) =>
|
||||||
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
|
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, {
|
||||||
|
autoCapitalization: state
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<Select
|
<Select
|
||||||
defaultValue={currentWorkspace?._id}
|
defaultValue={currentWorkspace?._id}
|
||||||
value={currentWorkspace?._id}
|
value={currentWorkspace?._id}
|
||||||
className="w-full py-2.5 bg-mineshaft-600 font-medium"
|
className="w-full py-2.5 bg-mineshaft-600 font-medium truncate"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
router.push(`/dashboard/${value}`);
|
router.push(`/dashboard/${value}`);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
@@ -17,7 +17,7 @@ interface Props {
|
|||||||
* @param {*} clientPublicKey
|
* @param {*} clientPublicKey
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const changePassword2 = ({
|
const changePassword2 = async ({
|
||||||
clientProof,
|
clientProof,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
@@ -27,29 +27,20 @@ const changePassword2 = ({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
}: Props) =>
|
}: Props) => {
|
||||||
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
const { data } = await apiRequest.post('/api/v1/password/change-password', {
|
||||||
method: 'POST',
|
clientProof,
|
||||||
headers: {
|
protectedKey,
|
||||||
'Content-Type': 'application/json'
|
protectedKeyIV,
|
||||||
},
|
protectedKeyTag,
|
||||||
body: JSON.stringify({
|
encryptedPrivateKey,
|
||||||
clientProof,
|
encryptedPrivateKeyIV,
|
||||||
protectedKey,
|
encryptedPrivateKeyTag,
|
||||||
protectedKeyIV,
|
salt,
|
||||||
protectedKeyTag,
|
verifier
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
})
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
console.log('Failed to change the password');
|
|
||||||
return undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export default changePassword2;
|
export default changePassword2;
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function Dashboard() {
|
|||||||
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
||||||
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
||||||
|
|
||||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
|
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
@@ -217,6 +217,9 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
if (router.isReady && workspaceId === 'undefined') {
|
||||||
|
router.push('/noprojects');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
||||||
workspaceId
|
workspaceId
|
||||||
@@ -314,7 +317,7 @@ export default function Dashboard() {
|
|||||||
valueOverride: undefined,
|
valueOverride: undefined,
|
||||||
comment: '',
|
comment: '',
|
||||||
tags: []
|
tags: []
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -332,27 +335,27 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modifyValue = (value: string, id: string) => {
|
const modifyValue = (value: string, id: string) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, value } : e)));
|
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, value } : e)));
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValueOverride = (valueOverride: string | undefined, id: string) => {
|
const modifyValueOverride = (valueOverride: string | undefined, id: string) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, valueOverride } : e)));
|
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, valueOverride } : e)));
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyKey = (key: string, id: string) => {
|
const modifyKey = (key: string, id: string) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, key } : e)));
|
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, key } : e)));
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyComment = (comment: string, id: string) => {
|
const modifyComment = (comment: string, id: string) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, comment } : e)));
|
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, comment } : e)));
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyTags = (tags: Tag[], id: string) => {
|
const modifyTags = (tags: Tag[], id: string) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, tags } : e)));
|
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, tags } : e)));
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,7 +447,8 @@ export default function Dashboard() {
|
|||||||
initialData!
|
initialData!
|
||||||
.filter(
|
.filter(
|
||||||
(initDataPoint) =>
|
(initDataPoint) =>
|
||||||
newData!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
newData!.filter((dataPoint) => dataPoint.id)
|
||||||
|
.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
||||||
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
|
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
|
||||||
initDataPoint.value ||
|
initDataPoint.value ||
|
||||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
||||||
@@ -477,7 +481,7 @@ export default function Dashboard() {
|
|||||||
const overridesToBeAdded = newOverrides!
|
const overridesToBeAdded = newOverrides!
|
||||||
.filter(
|
.filter(
|
||||||
(newDataPoint) =>
|
(newDataPoint) =>
|
||||||
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
|
!initOverrides.map((initDataPoint) => initDataPoint.idOverride).includes(newDataPoint.idOverride)
|
||||||
)
|
)
|
||||||
.map((override) => ({
|
.map((override) => ({
|
||||||
pos: override.pos,
|
pos: override.pos,
|
||||||
@@ -496,18 +500,21 @@ export default function Dashboard() {
|
|||||||
initOverrides
|
initOverrides
|
||||||
.filter(
|
.filter(
|
||||||
(initDataPoint) =>
|
(initDataPoint) =>
|
||||||
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
newOverrides!.map((dataPoint) => dataPoint.idOverride)
|
||||||
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
|
.includes(initDataPoint.idOverride) &&
|
||||||
|
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
|
||||||
.valueOverride !== initDataPoint.valueOverride ||
|
.valueOverride !== initDataPoint.valueOverride ||
|
||||||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0].key !==
|
||||||
initDataPoint.key ||
|
initDataPoint.key ||
|
||||||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
|
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
|
||||||
.comment !== initDataPoint.comment ||
|
.comment || '') !== (initDataPoint.comment || '')
|
||||||
JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
|
||
|
||||||
JSON.stringify(initDataPoint?.tags))
|
(JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]?.tags) || '') !==
|
||||||
|
(JSON.stringify(initDataPoint?.tags) || '')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.map((secret) => secret.id)
|
.map((secret) => secret.idOverride)
|
||||||
.includes(newDataPoint.id)
|
.includes(newDataPoint.idOverride)
|
||||||
)
|
)
|
||||||
.map((override) => ({
|
.map((override) => ({
|
||||||
pos: override.pos,
|
pos: override.pos,
|
||||||
@@ -665,7 +672,7 @@ export default function Dashboard() {
|
|||||||
idOverride: tempDecryptedSecrets.filter(
|
idOverride: tempDecryptedSecrets.filter(
|
||||||
(secret) => secret.key === key && secret.type === 'personal'
|
(secret) => secret.key === key && secret.type === 'personal'
|
||||||
)[0]?.id,
|
)[0]?.id,
|
||||||
pos: (newData?.filter(dp => !dp.id.includes('-'))?.length ?? 0) + index,
|
pos: (newData?.filter(dp => !dp.id?.includes('-'))?.length ?? 0) + index,
|
||||||
key,
|
key,
|
||||||
value: tempDecryptedSecrets.filter(
|
value: tempDecryptedSecrets.filter(
|
||||||
(secret) => secret.key === key && secret.type === 'shared'
|
(secret) => secret.key === key && secret.type === 'shared'
|
||||||
@@ -681,8 +688,8 @@ export default function Dashboard() {
|
|||||||
)[0]?.tags
|
)[0]?.tags
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setInitialData(structuredClone(newData?.filter(dp => !dp.id.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))));
|
setInitialData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))));
|
||||||
setData(structuredClone(newData?.filter(dp => !dp.id.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))))
|
setData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))))
|
||||||
} else {
|
} else {
|
||||||
setInitialData(structuredClone(newData));
|
setInitialData(structuredClone(newData));
|
||||||
}
|
}
|
||||||
@@ -746,11 +753,13 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOnEnvironmentChange = (envName: string) => {
|
const handleOnEnvironmentChange = (envName: string) => {
|
||||||
if(hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
|
const selectedWorkspaceEnv = workspaceEnvs.find(
|
||||||
|
({ name }: { name: string }) => envName === name
|
||||||
|
) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown',
|
slug: 'unknown',
|
||||||
isWriteDenied: false,
|
isWriteDenied: false,
|
||||||
@@ -839,16 +848,18 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="flex justify-start max-w-sm mt-1 mr-2">
|
<div className="flex justify-start max-w-sm mt-1 mr-2">
|
||||||
{!selectedEnv?.isReadDenied && <Button
|
{!selectedEnv?.isReadDenied && (
|
||||||
text={String(`${numSnapshots} ${t('Commits')}`)}
|
<Button
|
||||||
onButtonPressed={() => {
|
text={String(`${numSnapshots} ${t('Commits')}`)}
|
||||||
toggleSidebar('None');
|
onButtonPressed={() => {
|
||||||
togglePITSidebar(true)
|
toggleSidebar('None');
|
||||||
}}
|
togglePITSidebar(true);
|
||||||
color="mineshaft"
|
}}
|
||||||
size="md"
|
color="mineshaft"
|
||||||
icon={faClockRotateLeft}
|
size="md"
|
||||||
/>}
|
icon={faClockRotateLeft}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
|
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
|
||||||
<div className="flex justify-start max-w-sm mt-1">
|
<div className="flex justify-start max-w-sm mt-1">
|
||||||
@@ -882,17 +893,24 @@ export default function Dashboard() {
|
|||||||
comment: '',
|
comment: '',
|
||||||
tags: sv.tags
|
tags: sv.tags
|
||||||
}));
|
}));
|
||||||
setData(rolledBackSecrets);
|
|
||||||
|
|
||||||
// Perform the rollback globally
|
// Perform the rollback globally
|
||||||
performSecretRollback({ workspaceId, version: snapshotData.version });
|
const result = await performSecretRollback({ workspaceId, version: snapshotData.version });
|
||||||
|
if (result === undefined) {
|
||||||
setSnapshotData(undefined);
|
createNotification({
|
||||||
createNotification({
|
text: `Something went wrong during the rollback.`,
|
||||||
text: `Rollback has been performed successfully.`,
|
type: 'error'
|
||||||
type: 'success'
|
});
|
||||||
});
|
} else {
|
||||||
setHasUnsavedChanges(false);
|
setData(rolledBackSecrets);
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully rolled back secrets.`,
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
setSnapshotData(undefined);
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
togglePITSidebar(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -975,57 +993,68 @@ export default function Dashboard() {
|
|||||||
alt="infisical loading indicator"
|
alt="infisical loading indicator"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : data?.length !== 0 ? (
|
) : (data?.length !== 0 || snapshotData?.secretVersions) ? (
|
||||||
<div className="flex flex-col w-full mt-1">
|
<div className="flex flex-col w-full mt-1">
|
||||||
<div
|
<div
|
||||||
onScroll={onSecretsAreaScroll}
|
onScroll={onSecretsAreaScroll}
|
||||||
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
|
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
|
||||||
>
|
>
|
||||||
<div ref={secretsTop} />
|
<div ref={secretsTop} />
|
||||||
<div
|
<div className="group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]">
|
||||||
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||||
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
||||||
<div className='text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default'>0</div>
|
<div className="text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default">
|
||||||
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
|
0
|
||||||
{!snapshotData && <IconButton
|
</div>
|
||||||
ariaLabel="copy icon"
|
<span className="px-2 text-bunker-300 font-semibold">Key</span>
|
||||||
variant="plain"
|
{!snapshotData && (
|
||||||
className="group relative ml-2"
|
<IconButton
|
||||||
onClick={() => reorderRows(1)}
|
ariaLabel="copy icon"
|
||||||
>
|
variant="plain"
|
||||||
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
|
className="group relative ml-2"
|
||||||
</IconButton>}
|
onClick={() => reorderRows(1)}
|
||||||
|
>
|
||||||
|
{sortMethod === 'alphabetical' ? (
|
||||||
|
<FontAwesomeIcon icon={faArrowUp} />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faArrowDown} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-5/12 border-r border-mineshaft-600">
|
<div className="w-5/12 border-r border-mineshaft-600">
|
||||||
<div
|
<div className="flex items-center rounded-lg mt-4 md:mt-0 max-h-10">
|
||||||
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
|
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12">
|
||||||
>
|
Value
|
||||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
||||||
<div className="flex items-center max-h-16 overflow-hidden">
|
<div className="flex items-center max-h-16 overflow-hidden">
|
||||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
|
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
|
||||||
|
Comment
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/12">
|
<div className="w-2/12">
|
||||||
<div className="flex items-center max-h-16">
|
<div className="flex items-center max-h-16">
|
||||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
|
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center" />
|
||||||
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
|
<div className="w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center">
|
||||||
/>
|
|
||||||
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
|
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="none"
|
role="none"
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
className="invisible group-hover:visible"
|
className="invisible group-hover:visible"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
|
<FontAwesomeIcon
|
||||||
|
className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible"
|
||||||
|
icon={faXmark}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1033,12 +1062,18 @@ export default function Dashboard() {
|
|||||||
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
|
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
|
||||||
{!snapshotData &&
|
{!snapshotData &&
|
||||||
data
|
data
|
||||||
?.filter((row) =>
|
?.filter(
|
||||||
row.key?.toUpperCase().includes(searchKeys.toUpperCase())
|
(row) =>
|
||||||
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|
row.key?.toUpperCase().includes(searchKeys.toUpperCase()) ||
|
||||||
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
|
row.tags
|
||||||
|
?.map((tag) => tag.name)
|
||||||
|
.join(' ')
|
||||||
|
?.toUpperCase()
|
||||||
|
.includes(searchKeys.toUpperCase()) ||
|
||||||
|
row.comment?.toUpperCase().includes(searchKeys.toUpperCase())
|
||||||
|
)
|
||||||
.filter((row) => !sharedToHide.includes(row.id))
|
.filter((row) => !sharedToHide.includes(row.id))
|
||||||
.filter((row) => row.value !== undefined)
|
// .filter((row) => row.value !== undefined)
|
||||||
.map((keyPair) => (
|
.map((keyPair) => (
|
||||||
<KeyPair
|
<KeyPair
|
||||||
isCapitalized={autoCapitalization}
|
isCapitalized={autoCapitalization}
|
||||||
@@ -1066,22 +1101,6 @@ export default function Dashboard() {
|
|||||||
?.sort((a, b) => a.key.localeCompare(b.key))
|
?.sort((a, b) => a.key.localeCompare(b.key))
|
||||||
.filter((row) => row.environment === selectedSnapshotEnv?.slug)
|
.filter((row) => row.environment === selectedSnapshotEnv?.slug)
|
||||||
.filter((row) => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
.filter((row) => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||||
.filter(
|
|
||||||
(row) =>
|
|
||||||
!snapshotData.secretVersions
|
|
||||||
?.filter((secretVersion) =>
|
|
||||||
snapshotData.secretVersions
|
|
||||||
?.map((item) => item.key)
|
|
||||||
.filter(
|
|
||||||
(item, index) =>
|
|
||||||
index !==
|
|
||||||
snapshotData.secretVersions?.map((i) => i.key).indexOf(item)
|
|
||||||
)
|
|
||||||
.includes(secretVersion.key)
|
|
||||||
)
|
|
||||||
?.map((item) => item.id)
|
|
||||||
.includes(row.id)
|
|
||||||
)
|
|
||||||
.map((keyPair) => (
|
.map((keyPair) => (
|
||||||
<KeyPair
|
<KeyPair
|
||||||
isCapitalized={autoCapitalization}
|
isCapitalized={autoCapitalization}
|
||||||
@@ -1102,15 +1121,15 @@ export default function Dashboard() {
|
|||||||
tags={projectTags}
|
tags={projectTags}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
|
<div className="bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]">
|
||||||
<div className='w-10'/>
|
<div className="w-10" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className='text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer'
|
className="text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer"
|
||||||
onClick={addRowToBottom}
|
onClick={addRowToBottom}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} className='mr-3'/>
|
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||||
<span className='text-sm'>Add Secret</span>
|
<span className="text-sm">Add Secret</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1157,8 +1176,7 @@ export default function Dashboard() {
|
|||||||
<SideBar
|
<SideBar
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
data={data.filter(
|
data={data.filter(
|
||||||
(row: SecretDataProps) =>
|
(row: SecretDataProps) => row.id === sidebarSecretId && row.value !== undefined
|
||||||
row.id === sidebarSecretId && row.value !== undefined
|
|
||||||
)}
|
)}
|
||||||
modifyKey={listenChangeKey}
|
modifyKey={listenChangeKey}
|
||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
|
|||||||
@@ -1,374 +1,21 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { plans } from 'public/data/frequentConstants';
|
|
||||||
|
|
||||||
import Button from '@app/components/basic/buttons/Button';
|
|
||||||
import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
|
|
||||||
import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
|
|
||||||
import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
|
|
||||||
import InputField from '@app/components/basic/InputField';
|
|
||||||
import UserTable from '@app/components/basic/table/UserTable';
|
|
||||||
import NavHeader from '@app/components/navigation/NavHeader';
|
|
||||||
import guidGenerator from '@app/components/utilities/randomId';
|
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
|
||||||
import addUserToOrg from '../../api/organization/addUserToOrg';
|
|
||||||
import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
|
|
||||||
import getIncidentContacts from '../../api/organization/getIncidentContacts';
|
|
||||||
import getOrganization from '../../api/organization/GetOrg';
|
|
||||||
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
|
|
||||||
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
|
|
||||||
import renameOrg from '../../api/organization/renameOrg';
|
|
||||||
import getUser from '../../api/user/getUser';
|
|
||||||
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
|
||||||
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
|
||||||
|
|
||||||
export default function SettingsOrg() {
|
export default function SettingsOrg() {
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
const host = window.location.origin;
|
|
||||||
const [orgName, setOrgName] = useState('');
|
|
||||||
const [emailUser, setEmailUser] = useState('');
|
|
||||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
|
||||||
const [searchUsers, setSearchUsers] = useState('');
|
|
||||||
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
|
|
||||||
const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
|
|
||||||
const [incidentContacts, setIncidentContacts] = useState<string[]>([]);
|
|
||||||
const [searchIncidentContact, setSearchIncidentContact] = useState('');
|
|
||||||
const [userList, setUserList] = useState<any[]>([]);
|
|
||||||
const [personalEmail, setPersonalEmail] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [currentPlan, setCurrentPlan] = useState('');
|
|
||||||
|
|
||||||
const workspaceId = router.query.id as string;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const orgId = localStorage.getItem('orgData.id') as string;
|
|
||||||
const org = await getOrganization({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
setOrgName(org.name);
|
|
||||||
const incidentContactsData = await getIncidentContacts(
|
|
||||||
localStorage.getItem('orgData.id') as string
|
|
||||||
);
|
|
||||||
|
|
||||||
setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
|
|
||||||
|
|
||||||
const user = await getUser();
|
|
||||||
setPersonalEmail(user.email);
|
|
||||||
|
|
||||||
const orgUsers = await getOrganizationUsers({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
setUserList(
|
|
||||||
orgUsers.map((orgUser) => ({
|
|
||||||
key: guidGenerator(),
|
|
||||||
firstName: orgUser.user?.firstName,
|
|
||||||
lastName: orgUser.user?.lastName,
|
|
||||||
email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
|
|
||||||
role: orgUser?.role,
|
|
||||||
status: orgUser?.status,
|
|
||||||
userId: orgUser.user?._id,
|
|
||||||
membershipId: orgUser._id,
|
|
||||||
publicKey: orgUser.user?.publicKey
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const subscriptions = await getOrganizationSubscriptions({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
if (subscriptions) {
|
|
||||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const modifyOrgName = (newName: string) => {
|
|
||||||
setButtonReady(true);
|
|
||||||
setOrgName(newName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitChanges = (newOrgName: string) => {
|
|
||||||
renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
|
|
||||||
setButtonReady(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAddUserModal = () => {
|
|
||||||
setIsAddUserOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAddIncidentContactModal = () => {
|
|
||||||
setIsAddIncidentContactOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddUserModal = () => {
|
|
||||||
setIsAddUserOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddIncidentContactModal = () => {
|
|
||||||
setIsAddIncidentContactOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitAddUserModal = async (newUserEmail: string) => {
|
|
||||||
await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
|
|
||||||
setEmail('');
|
|
||||||
setIsAddUserOpen(false);
|
|
||||||
router.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteIncidentContactFully = (incidentContact: string) => {
|
|
||||||
setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
|
|
||||||
deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function deleted a workspace.
|
|
||||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
|
||||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
|
||||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
|
||||||
*/
|
|
||||||
const executeDeletingWorkspace = async () => {
|
|
||||||
const userWorkspaces = await getWorkspaces();
|
|
||||||
|
|
||||||
if (userWorkspaces.length > 1) {
|
|
||||||
if (
|
|
||||||
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
|
|
||||||
workspaceToBeDeletedName
|
|
||||||
) {
|
|
||||||
await deleteWorkspace(workspaceId);
|
|
||||||
const ws = await getWorkspaces();
|
|
||||||
router.push(`/dashboard/${ws[0]._id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
|
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<OrgSettingsPage />
|
||||||
<div className="w-full max-h-screen pb-2">
|
</>
|
||||||
<NavHeader pageName={t('settings-org:title')} />
|
|
||||||
<AddIncidentContactDialog
|
|
||||||
isOpen={isAddIncidentContactOpen}
|
|
||||||
closeModal={closeAddIncidentContactModal}
|
|
||||||
incidentContacts={incidentContacts}
|
|
||||||
setIncidentContacts={setIncidentContacts}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
|
||||||
<div className="flex flex-col justify-start items-start text-3xl">
|
|
||||||
<p className="font-semibold mr-4 text-gray-200">{t('settings-org:title')}</p>
|
|
||||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
|
||||||
{t('settings-org:description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-8xl">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
|
|
||||||
<div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start w-full mb-6">
|
|
||||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
|
||||||
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
|
|
||||||
{t('common:display-name')}
|
|
||||||
</p>
|
|
||||||
<InputField
|
|
||||||
label=""
|
|
||||||
// label="Organization Name"
|
|
||||||
onChangeHandler={modifyOrgName}
|
|
||||||
type="varName"
|
|
||||||
value={orgName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start w-full">
|
|
||||||
<div className="flex justify-start max-w-sm mt-4 mb-2">
|
|
||||||
<Button
|
|
||||||
text={t('common:save-changes') as string}
|
|
||||||
onButtonPressed={() => submitChanges(orgName)}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
active={buttonReady}
|
|
||||||
iconDisabled={faCheck}
|
|
||||||
textDisabled={t('common:saved') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6">
|
|
||||||
<p className="font-semibold mr-4 text-white text-xl">
|
|
||||||
{t('section-members:org-members')}
|
|
||||||
</p>
|
|
||||||
<p className="mr-4 text-gray-400 mt-2 mb-2">
|
|
||||||
{t('section-members:org-members-description')}
|
|
||||||
</p>
|
|
||||||
<AddUserDialog
|
|
||||||
isOpen={isAddUserOpen && (userList.length < 5 || currentPlan !== plans.starter || host !== 'https://app.infisical.com')}
|
|
||||||
closeModal={closeAddUserModal}
|
|
||||||
submitModal={submitAddUserModal}
|
|
||||||
email={emailUser}
|
|
||||||
setEmail={setEmailUser}
|
|
||||||
orgName={orgName}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={isAddUserOpen && userList.length >= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
|
|
||||||
onClose={closeAddUserModal}
|
|
||||||
text="You can add more members if you switch to Infisical's Team plan."
|
|
||||||
/>
|
|
||||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
|
||||||
<div className="pb-1 w-full flex flex-row items-start">
|
|
||||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
|
||||||
icon={faMagnifyingGlass}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
|
||||||
value={searchUsers}
|
|
||||||
onChange={(e) => setSearchUsers(e.target.value)}
|
|
||||||
placeholder={t('section-members:search-members') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
|
|
||||||
<Button
|
|
||||||
text={t('section-members:add-member') as string}
|
|
||||||
onButtonPressed={openAddUserModal}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
icon={faPlus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{userList && (
|
|
||||||
<div className="overflow-y-auto w-full">
|
|
||||||
<UserTable
|
|
||||||
userData={userList}
|
|
||||||
changeData={setUserList}
|
|
||||||
myUser={personalEmail}
|
|
||||||
filter={searchUsers.toLowerCase()}
|
|
||||||
resendInvite={submitAddUserModal}
|
|
||||||
isOrg
|
|
||||||
// onClick={openDeleteModal}
|
|
||||||
// deleteUser={deleteMembership}
|
|
||||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4">
|
|
||||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
|
||||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
|
||||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
|
||||||
{t('section-incident:incident-contacts')}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mb-2 min-w-max">
|
|
||||||
{t('section-incident:incident-contacts-description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-center">
|
|
||||||
<Button
|
|
||||||
text={t('section-incident:add-contact') as string}
|
|
||||||
onButtonPressed={openAddIncidentContactModal}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
icon={faPlus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flwex-row">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400"
|
|
||||||
icon={faMagnifyingGlass}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none"
|
|
||||||
value={searchIncidentContact}
|
|
||||||
onChange={(e) => setSearchIncidentContact(e.target.value)}
|
|
||||||
placeholder={t('common:search') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{incidentContacts?.filter((incidentEmail) =>
|
|
||||||
incidentEmail.includes(searchIncidentContact)
|
|
||||||
).length > 0 ? (
|
|
||||||
incidentContacts
|
|
||||||
.filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
|
|
||||||
.map((contact) => (
|
|
||||||
<div
|
|
||||||
key={guidGenerator()}
|
|
||||||
className="flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full"
|
|
||||||
>
|
|
||||||
<p className="text-gray-300">{contact}</p>
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => deleteIncidentContactFully(contact)}
|
|
||||||
color="red"
|
|
||||||
size="icon-sm"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex flex-row justify-center mt-6 max-w-4xl ml-6">
|
|
||||||
<p className="text-gray-400">{t('section-incident:no-incident-contacts')}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
|
||||||
<p className="text-xl font-bold text-red">
|
|
||||||
Danger Zone
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 text-md text-gray-400">
|
|
||||||
As soon as you delete an organization, you will
|
|
||||||
not be able to undo it. This will immediately
|
|
||||||
remove all organization members and cancel your
|
|
||||||
subscription. If you still want to do that,
|
|
||||||
please enter the name of the organization below.
|
|
||||||
</p>
|
|
||||||
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
|
||||||
<InputField
|
|
||||||
label="Organization to be Deleted"
|
|
||||||
onChangeHandler={
|
|
||||||
setWorkspaceToBeDeletedName
|
|
||||||
}
|
|
||||||
type="varName"
|
|
||||||
value={workspaceToBeDeletedName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
|
||||||
onClick={executeDeletingWorkspace}
|
|
||||||
>
|
|
||||||
Delete Project
|
|
||||||
</button>
|
|
||||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
|
||||||
Note: You can only delete a project in case you
|
|
||||||
have more than one.
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { plans } from 'public/data/frequentConstants';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
|
||||||
|
import {
|
||||||
|
useAddIncidentContact,
|
||||||
|
useAddUserToOrg,
|
||||||
|
useDeleteIncidentContact,
|
||||||
|
useDeleteOrgMembership,
|
||||||
|
useGetOrgIncidentContact,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetUserWorkspaceMemberships,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useRenameOrg,
|
||||||
|
useUpdateOrgUserRole,
|
||||||
|
useUploadWsKey
|
||||||
|
} from '@app/hooks/api';
|
||||||
|
|
||||||
|
import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
|
||||||
|
|
||||||
|
export const OrgSettingsPage = () => {
|
||||||
|
const host = window.location.origin;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { subscriptionPlan } = useSubscription();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const orgId = currentOrg?._id || '';
|
||||||
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
|
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
|
||||||
|
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
|
||||||
|
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
|
||||||
|
|
||||||
|
const renameOrg = useRenameOrg();
|
||||||
|
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||||
|
const addUserToOrg = useAddUserToOrg();
|
||||||
|
const updateOrgUserRole = useUpdateOrgUserRole();
|
||||||
|
const uploadWsKey = useUploadWsKey();
|
||||||
|
const addIncidentContact = useAddIncidentContact();
|
||||||
|
const removeIncidentContact = useDeleteIncidentContact();
|
||||||
|
|
||||||
|
const isMoreUsersNotAllowed =
|
||||||
|
(orgUsers || []).length >= 5 &&
|
||||||
|
subscriptionPlan === plans.starter &&
|
||||||
|
host === 'https://app.infisical.com';
|
||||||
|
|
||||||
|
const onRenameOrg = async (name: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully renamed organization',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to rename organization',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveUserOrgMembership = async (membershipId: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully removed used from org',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove user from org',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onAddUserToOrg = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully invited user to org',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to invite user to org',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateOrgUserRole = async (membershipId: string, role: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully updated user role',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to update user role',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGrantUserAccess = async (userId: string, publicKey: string) => {
|
||||||
|
try {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
if (!PRIVATE_KEY || !wsKey) return;
|
||||||
|
|
||||||
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: wsKey.encryptedKey,
|
||||||
|
nonce: wsKey.nonce,
|
||||||
|
publicKey: wsKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadWsKey.mutateAsync({
|
||||||
|
userId,
|
||||||
|
nonce,
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
workspaceId: currentWorkspace?._id || ''
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to grant access to user',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddIncidentContact = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addIncidentContact.mutateAsync({ orgId, email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully added incident contact',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to add incident contact',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveIncidentContact = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeIncidentContact.mutateAsync({ orgId, email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully removed incident contact',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove incident contact',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function deleted a workspace.
|
||||||
|
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||||
|
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||||
|
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||||
|
*/
|
||||||
|
// const executeDeletingWorkspace = async () => {
|
||||||
|
// const userWorkspaces = await getWorkspaces();
|
||||||
|
//
|
||||||
|
// if (userWorkspaces.length > 1) {
|
||||||
|
// if (
|
||||||
|
// userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
|
||||||
|
// workspaceToBeDeletedName
|
||||||
|
// ) {
|
||||||
|
// await deleteWorkspace(workspaceId);
|
||||||
|
// const ws = await getWorkspaces();
|
||||||
|
// router.push(`/dashboard/${ws[0]._id}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
<NavHeader pageName={t('settings-org:title')} />
|
||||||
|
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||||
|
<div className="flex flex-col items-start justify-start text-3xl">
|
||||||
|
<p className="mr-4 font-semibold text-gray-200">{t('settings-org:title')}</p>
|
||||||
|
<p className="mr-4 text-base font-normal text-gray-400">
|
||||||
|
{t('settings-org:description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
||||||
|
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
|
||||||
|
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||||
|
<p className="mr-4 text-xl font-semibold text-white">
|
||||||
|
{t('section-members:org-members')}
|
||||||
|
</p>
|
||||||
|
<p className="mr-4 mt-2 mb-2 text-gray-400">
|
||||||
|
{t('section-members:org-members-description')}
|
||||||
|
</p>
|
||||||
|
<OrgMembersTable
|
||||||
|
isMoreUserNotAllowed={isMoreUsersNotAllowed}
|
||||||
|
orgName={currentOrg?.name || ''}
|
||||||
|
members={orgUsers}
|
||||||
|
workspaceMemberships={workspaceMemberships}
|
||||||
|
onInviteMember={onAddUserToOrg}
|
||||||
|
userId={user?._id || ''}
|
||||||
|
onRemoveMember={onRemoveUserOrgMembership}
|
||||||
|
onRoleChange={onUpdateOrgUserRole}
|
||||||
|
onGrantAccess={onGrantUserAccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||||
|
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||||
|
<div className="flex w-full max-w-3xl flex-col justify-between">
|
||||||
|
<p className="mb-3 min-w-max text-xl font-semibold">
|
||||||
|
{t('section-incident:incident-contacts')}
|
||||||
|
</p>
|
||||||
|
<p className="mb-2 min-w-max text-xs text-gray-500">
|
||||||
|
{t('section-incident:incident-contacts-description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<OrgIncidentContactsTable
|
||||||
|
contacts={incidentContact}
|
||||||
|
onRemoveContact={onRemoveIncidentContact}
|
||||||
|
onAddContact={onAddIncidentContact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
||||||
|
<p className="text-xl font-bold text-red">
|
||||||
|
Danger Zone
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-md text-gray-400">
|
||||||
|
As soon as you delete an organization, you will
|
||||||
|
not be able to undo it. This will immediately
|
||||||
|
remove all organization members and cancel your
|
||||||
|
subscription. If you still want to do that,
|
||||||
|
please enter the name of the organization below.
|
||||||
|
</p>
|
||||||
|
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
||||||
|
<InputField
|
||||||
|
label="Organization to be Deleted"
|
||||||
|
onChangeHandler={
|
||||||
|
setWorkspaceToBeDeletedName
|
||||||
|
}
|
||||||
|
type="varName"
|
||||||
|
value={workspaceToBeDeletedName}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
onClick={executeDeletingWorkspace}
|
||||||
|
>
|
||||||
|
Delete Project
|
||||||
|
</button>
|
||||||
|
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||||
|
Note: You can only delete a project in case you
|
||||||
|
have more than one.
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { IncidentContact } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contacts?: IncidentContact[];
|
||||||
|
onRemoveContact: (email: string) => Promise<void>;
|
||||||
|
onAddContact: (email: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addContactFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label('Email').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||||
|
|
||||||
|
export const OrgIncidentContactsTable = ({
|
||||||
|
contacts = [],
|
||||||
|
onAddContact,
|
||||||
|
onRemoveContact
|
||||||
|
}: Props) => {
|
||||||
|
const [searchContact, setSearchContact] = useState('');
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
'addContact',
|
||||||
|
'removeContact'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||||
|
|
||||||
|
const onAddIncidentContact = ({ email }: TAddContactForm) => {
|
||||||
|
onAddContact(email);
|
||||||
|
handlePopUpClose('addContact');
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveIncidentContact = async () => {
|
||||||
|
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||||
|
await onRemoveContact(incidentContactEmail);
|
||||||
|
handlePopUpClose('removeContact');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchContact}
|
||||||
|
onChange={(e) => setSearchContact(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search incident contact by email..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen('addContact')}
|
||||||
|
>
|
||||||
|
Add Contact
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{contacts
|
||||||
|
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||||
|
?.map(({ email }) => (
|
||||||
|
<Tr key={email}>
|
||||||
|
<Td className="w-full">{email}</Td>
|
||||||
|
<Td className="mr-4">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen('removeContact', { email })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{contacts
|
||||||
|
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||||
|
?.length === 0 && (
|
||||||
|
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addContact?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle('addContact', isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add an Incident Contact"
|
||||||
|
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddIncidentContact)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Incident Contact
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose('addContact')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeContact.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this email from incident contact?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
|
||||||
|
onDeleteApproved={onRemoveIncidentContact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
Tag,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { OrgUser, Workspace } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
members?: OrgUser[];
|
||||||
|
workspaceMemberships?: Record<string, Workspace[]>;
|
||||||
|
orgName: string;
|
||||||
|
isMoreUserNotAllowed: boolean;
|
||||||
|
onRemoveMember: (userId: string) => Promise<void>;
|
||||||
|
onInviteMember: (email: string) => Promise<void>;
|
||||||
|
onRoleChange: (membershipId: string, role: string) => Promise<void>;
|
||||||
|
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
||||||
|
// the current user id to block remove org button
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMemberFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label('Email').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
|
export const OrgMembersTable = ({
|
||||||
|
members = [],
|
||||||
|
workspaceMemberships = {},
|
||||||
|
orgName,
|
||||||
|
isMoreUserNotAllowed,
|
||||||
|
onRemoveMember,
|
||||||
|
onInviteMember,
|
||||||
|
onGrantAccess,
|
||||||
|
onRoleChange,
|
||||||
|
userId
|
||||||
|
}: Props) => {
|
||||||
|
const [searchMemberFilter, setSearchMemberFilter] = useState('');
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
'addMember',
|
||||||
|
'removeMember',
|
||||||
|
'upgradePlan'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
|
const onAddMember = ({ email }: TAddMemberForm) => {
|
||||||
|
onInviteMember(email);
|
||||||
|
handlePopUpClose('addMember');
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveOrgMemberApproved = async () => {
|
||||||
|
const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||||
|
await onRemoveMember(orgMembershipId);
|
||||||
|
handlePopUpClose('removeMember');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIamOwner = useMemo(
|
||||||
|
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
|
||||||
|
[userId, members]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterdUser = useMemo(
|
||||||
|
() =>
|
||||||
|
members.filter(
|
||||||
|
({ user, inviteEmail }) =>
|
||||||
|
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
|
),
|
||||||
|
[members, searchMemberFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchMemberFilter}
|
||||||
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search members..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
if (isMoreUserNotAllowed) {
|
||||||
|
handlePopUpOpen('upgradePlan');
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen('addMember');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th>Role</Th>
|
||||||
|
<Th>Projects</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
|
const name = user ? `${user.firstName} ${user.lastName}` : '-';
|
||||||
|
const email = user?.email || inviteEmail;
|
||||||
|
const userWs = workspaceMemberships?.[user?._id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
{status === 'accepted' && (
|
||||||
|
<Select
|
||||||
|
defaultValue={role}
|
||||||
|
isDisabled={userId === user?._id}
|
||||||
|
className="w-full bg-mineshaft-600"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(isIamOwner || role === 'owner') && (
|
||||||
|
<SelectItem value="owner">owner</SelectItem>
|
||||||
|
)}
|
||||||
|
<SelectItem value="admin">admin</SelectItem>
|
||||||
|
<SelectItem value="member">member</SelectItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{(status === 'invited' || status === 'verified') && (
|
||||||
|
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
|
||||||
|
Resent Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{status === 'completed' && (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
||||||
|
>
|
||||||
|
Grant Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userWs ? (
|
||||||
|
userWs?.map(({ name: wsName, _id }) => (
|
||||||
|
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
||||||
|
{wsName}
|
||||||
|
</Tag>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Tag colorSchema="red">This user isn't part of any projects yet</Tag>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== user?._id && <IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
isDisabled={userId === user?._id}
|
||||||
|
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle('addMember', isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={`Invite others to ${orgName}`}
|
||||||
|
subTitle={
|
||||||
|
<>
|
||||||
|
An invite is specific to an email address and expires after 1 day.
|
||||||
|
<br />
|
||||||
|
For security reasons, you will need to separately add members to projects.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose('addMember')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeMember.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this user from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('removeMember', isOpen)}
|
||||||
|
onDeleteApproved={onRemoveOrgMemberApproved}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
|
||||||
|
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersTable } from './OrgMembersTable';
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import { Button, FormControl, Input } from '@app/components/v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orgName?: string;
|
||||||
|
onOrgNameChange: (name: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = yup.object({
|
||||||
|
name: yup.string().required().label('Project Name')
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
|
export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({ name: orgName });
|
||||||
|
}, [orgName]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name }: FormData) => {
|
||||||
|
await onOrgNameChange(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-3">
|
||||||
|
<p className="mb-4 mt-2 text-xl font-semibold">{t('common:display-name')}</p>
|
||||||
|
<div className="mb-2 w-full max-w-lg">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input placeholder="Type your org name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
color="mineshaft"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty || isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||||
|
>
|
||||||
|
{t('common:save-changes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||||
|
export { OrgMembersTable } from './OrgMembersTable';
|
||||||
|
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgSettingsPage } from './OrgSettingsPage';
|
||||||
+13
-2
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import { Checkbox } from '@app/components/v2';
|
import { Checkbox } from '@app/components/v2';
|
||||||
|
|
||||||
import { useGetUser } from '../../../../hooks/api';
|
import { useGetUser } from '../../../../hooks/api';
|
||||||
@@ -9,6 +10,7 @@ import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
|
|||||||
export const SecuritySection = () => {
|
export const SecuritySection = () => {
|
||||||
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
||||||
const { data: user } = useGetUser();
|
const { data: user } = useGetUser();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && typeof user.isMfaEnabled !== 'undefined') {
|
if (user && typeof user.isMfaEnabled !== 'undefined') {
|
||||||
@@ -21,18 +23,27 @@ export const SecuritySection = () => {
|
|||||||
const newUser: User = await updateMyMfaEnabled({
|
const newUser: User = await updateMyMfaEnabled({
|
||||||
isMfaEnabled: state
|
isMfaEnabled: state
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
setIsMfaEnabled(newUser.isMfaEnabled);
|
setIsMfaEnabled(newUser.isMfaEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `${newUser.isMfaEnabled ? 'Successfully turned on two-factor authentication.' : 'Successfully turned off two-factor authentication.'}`,
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: 'Something went wrong while toggling the two-factor authentication.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
||||||
<p className="mb-4 mt-2 text-xl font-semibold">
|
<p className="mb-4 mt-2 text-xl font-semibold">
|
||||||
Two-factor Authentication
|
Two-factor Authentication
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
|
|
||||||
const renameWorkspace = useRenameWorkspace();
|
const renameWorkspace = useRenameWorkspace();
|
||||||
const toggleAutoCapitalization = useToggleAutoCapitalization();
|
const toggleAutoCapitalization = useToggleAutoCapitalization();
|
||||||
|
|
||||||
const deleteWorkspace = useDeleteWorkspace();
|
const deleteWorkspace = useDeleteWorkspace();
|
||||||
// env crud operation
|
// env crud operation
|
||||||
const createWsEnv = useCreateWsEnvironment();
|
const createWsEnv = useCreateWsEnvironment();
|
||||||
@@ -97,7 +97,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAutoCapitalizationToggle = async (state: boolean) => {
|
const onAutoCapitalizationToggle = async (state: boolean) => {
|
||||||
try {
|
try {
|
||||||
await toggleAutoCapitalization.mutateAsync({
|
await toggleAutoCapitalization.mutateAsync({
|
||||||
workspaceID,
|
workspaceID,
|
||||||
@@ -123,6 +123,9 @@ export const ProjectSettingsPage = () => {
|
|||||||
await deleteWorkspace.mutateAsync({ workspaceID });
|
await deleteWorkspace.mutateAsync({ workspaceID });
|
||||||
// redirect user to first workspace user is part of
|
// redirect user to first workspace user is part of
|
||||||
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
|
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
|
||||||
|
if (!ws) {
|
||||||
|
router.push('/noprojects');
|
||||||
|
}
|
||||||
router.push(`/dashboard/${ws?._id}`);
|
router.push(`/dashboard/${ws?._id}`);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: 'Successfully deleted workspace',
|
text: 'Successfully deleted workspace',
|
||||||
@@ -247,7 +250,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
const res = await createWsTag.mutateAsync({
|
const res = await createWsTag.mutateAsync({
|
||||||
workspaceID,
|
workspaceID,
|
||||||
tagName: name,
|
tagName: name,
|
||||||
tagSlug: name.replace(" ", "_")
|
tagSlug: name.replace(' ', '_')
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
text: 'Successfully created a tag',
|
text: 'Successfully created a tag',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.14
|
version: 0.1.15
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
@@ -23,4 +23,4 @@ dependencies:
|
|||||||
- name: mailhog
|
- name: mailhog
|
||||||
version: "~5.2.3"
|
version: "~5.2.3"
|
||||||
repository: "https://codecentric.github.io/helm-charts"
|
repository: "https://codecentric.github.io/helm-charts"
|
||||||
condition: mailhog.enabled
|
condition: mailhog.enabled
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ mongodb:
|
|||||||
##
|
##
|
||||||
databases:
|
databases:
|
||||||
- "infisical"
|
- "infisical"
|
||||||
|
rootPassword: root
|
||||||
|
rootUser: root
|
||||||
## MongoDB persistence configuration
|
## MongoDB persistence configuration
|
||||||
##
|
##
|
||||||
persistence:
|
persistence:
|
||||||
@@ -250,7 +252,7 @@ ingress:
|
|||||||
kubernetes.io/ingress.class: "nginx"
|
kubernetes.io/ingress.class: "nginx"
|
||||||
# cert-manager.io/issuer: letsencrypt-nginx
|
# cert-manager.io/issuer: letsencrypt-nginx
|
||||||
## @param ingress.hostName Ingress hostname (your custom domain name)
|
## @param ingress.hostName Ingress hostname (your custom domain name)
|
||||||
## Replace with your own domain
|
## Replace with your own domain
|
||||||
##
|
##
|
||||||
hostName: infisical.local
|
hostName: infisical.local
|
||||||
## @skip ingress.frontend
|
## @skip ingress.frontend
|
||||||
@@ -264,9 +266,10 @@ ingress:
|
|||||||
path: /api
|
path: /api
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
## @param ingress.tls Ingress TLS hosts (matching above hostName)
|
## @param ingress.tls Ingress TLS hosts (matching above hostName)
|
||||||
## Replace with your own domain
|
## Replace with your own domain
|
||||||
##
|
##
|
||||||
tls: []
|
tls:
|
||||||
|
[]
|
||||||
# - secretName: letsencrypt-nginx
|
# - secretName: letsencrypt-nginx
|
||||||
# hosts:
|
# hosts:
|
||||||
# - infisical.local
|
# - infisical.local
|
||||||
@@ -338,7 +341,8 @@ mailhog:
|
|||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
## @param mailhog.ingress.annotations Ingress annotations
|
## @param mailhog.ingress.annotations Ingress annotations
|
||||||
##
|
##
|
||||||
annotations: {}
|
annotations:
|
||||||
|
{}
|
||||||
# kubernetes.io/ingress.class: nginx
|
# kubernetes.io/ingress.class: nginx
|
||||||
# kubernetes.io/tls-acme: "true"
|
# kubernetes.io/tls-acme: "true"
|
||||||
## @param mailhog.ingress.labels Ingress labels
|
## @param mailhog.ingress.labels Ingress labels
|
||||||
@@ -352,4 +356,4 @@ mailhog:
|
|||||||
##
|
##
|
||||||
paths:
|
paths:
|
||||||
- path: "/"
|
- path: "/"
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
|
|||||||
Reference in New Issue
Block a user