Merge remote-tracking branch 'origin' into mfa

This commit is contained in:
Tuan Dang
2023-02-21 11:31:17 +07:00
53 changed files with 1587 additions and 702 deletions
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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**
+17 -8
View File
@@ -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)) {
+33 -33
View File
@@ -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 {
+26 -8
View File
@@ -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
+6 -2
View File
@@ -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(
+8 -3
View File
@@ -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}
+5 -5
View File
@@ -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>
); );
} }
+1 -1
View File
@@ -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;
}; };
+20 -9
View File
@@ -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}
+24
View File
@@ -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>
);
+1
View File
@@ -0,0 +1 @@
export { Tag } from './Tag';
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+4 -1
View File
@@ -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,
+4 -1
View File
@@ -1,7 +1,10 @@
export { export {
fetchOrgUsers, fetchOrgUsers,
useAddUserToOrg,
useAddUserToWs, useAddUserToWs,
useDeleteOrgMembership,
useGetOrgUsers, useGetOrgUsers,
useGetUser, useGetUser,
useLogoutUser useLogoutUser,
useUpdateOrgUserRole
} from './queries'; } from './queries';
+56 -3
View File
@@ -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'),
+17 -1
View File
@@ -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;
};
+1 -1
View File
@@ -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';
+24 -5
View File
@@ -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);
} }
+1 -1
View File
@@ -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}`);
}} }}
+16 -25
View File
@@ -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;
+115 -97
View File
@@ -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}
+4 -357
View File
@@ -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>
);
};
@@ -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>
);
};
@@ -0,0 +1 @@
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
@@ -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&apos;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';
@@ -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';
@@ -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',
+2 -2
View File
@@ -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
+9 -5
View File
@@ -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