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

6
.github/values.yaml vendored
View File

@@ -2,7 +2,8 @@ frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/frontend
@@ -20,7 +21,8 @@ backend:
enabled: true
name: backend
podAnnotations: {}
deploymentAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/backend

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
- **[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
- 🔜 **2FA** (next week)
- **2FA** (more options coming soon)
- 🔜 **1-Click Deploy** to AWS
- 🔜 **Automatic Secret Rotation**
- 🔜 **Smart Security Alerts**

View File

@@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User } from '../../models';
import { User, LoginSRPDetail } from '../../models';
import { issueAuthTokens, createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
@@ -13,6 +13,7 @@ import {
JWT_MFA_LIFETIME,
JWT_MFA_SECRET
} from '../../config';
import { BadRequestError } from '../../utils/errors';
import {
TOKEN_EMAIL_MFA
} from '../../variables';
@@ -50,13 +51,15 @@ export const login1 = async (req: Request, res: Response) => {
salt: user.salt,
verifier: user.verifier
},
() => {
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
clientPublicKeys[email] = {
clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
};
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
@@ -89,15 +92,21 @@ export const login2 = async (req: Request, res: Response) => {
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();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: clientPublicKeys[email].serverBInt
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {

View File

@@ -138,7 +138,7 @@ const exchangeCodeAzure = async ({
try {
res = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
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`
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err: any) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
@@ -175,36 +175,36 @@ const exchangeCodeAzure = async ({
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeHeroku = async ({
code
code
}: {
code: string;
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date();
try {
res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_secret: CLIENT_SECRET_HEROKU
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date();
try {
res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_secret: CLIENT_SECRET_HEROKU
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
/**
@@ -234,7 +234,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
} catch (err) {
Sentry.setUser(null);
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 {

View File

@@ -30,7 +30,6 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
import { access, appendFile } from "fs";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -181,7 +180,8 @@ const syncSecretsAzureKeyVault = async ({
while (url) {
const res = await axios.get(url, {
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`, {
headers: {
'Authorization': `Bearer ${accessToken}`
'Authorization': `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
@@ -259,7 +260,8 @@ const syncSecretsAzureKeyVault = async ({
},
{
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
@@ -270,7 +272,8 @@ const syncSecretsAzureKeyVault = async ({
deleteSecrets.forEach(async (secret) => {
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
'Authorization': `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
});
@@ -488,6 +491,7 @@ const syncSecretsHeroku = async ({
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
)
@@ -506,6 +510,7 @@ const syncSecretsHeroku = async ({
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -552,7 +557,7 @@ const syncSecretsVercel = async ({
}
: {}),
};
const res = (
await Promise.all(
(
@@ -561,7 +566,8 @@ const syncSecretsVercel = async ({
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
))
@@ -573,7 +579,8 @@ const syncSecretsVercel = async ({
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
)).data)
@@ -633,6 +640,7 @@ const syncSecretsVercel = async ({
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -649,6 +657,7 @@ const syncSecretsVercel = async ({
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -664,6 +673,7 @@ const syncSecretsVercel = async ({
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -723,6 +733,7 @@ const syncSecretsNetlify = async ({
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
)
@@ -837,6 +848,7 @@ const syncSecretsNetlify = async ({
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -854,6 +866,7 @@ const syncSecretsNetlify = async ({
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -868,6 +881,7 @@ const syncSecretsNetlify = async ({
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -882,6 +896,7 @@ const syncSecretsNetlify = async ({
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -1035,6 +1050,7 @@ const syncSecretsRender = async ({
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
@@ -1088,6 +1104,7 @@ const syncSecretsFlyio = async ({
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
'Accept-Encoding': 'application/json'
},
data: {
query: SetSecrets,
@@ -1167,6 +1184,7 @@ const syncSecretsFlyio = async ({
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
'Accept-Encoding': 'application/json'
},
data: {
query: DeleteSecrets,

View File

@@ -13,6 +13,7 @@ export interface SecretDataProps {
value: string | undefined;
valueOverride: string | undefined;
id: string;
idOverride?: string;
comment: string;
tags: Tag[];
}

View File

@@ -30,7 +30,7 @@ type Props = {
type EnvironmentProps = {
name: string;
slug: string;
}
};
/**
* 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 ) => {
let denials: { ability: string; environmentSlug: string; }[];
if (val === "Read Only") {
denials = [{
ability: "write",
environmentSlug: slug
}];
} else if (val === "No Access") {
denials = [{
ability: "write",
environmentSlug: slug
}, {
ability: "read",
environmentSlug: slug
}];
} else if (val === "Add Only") {
denials = [{
ability: "read",
environmentSlug: slug
}];
const handlePermissionUpdate = (
index: number,
val: string,
membershipId: string,
slug: string
) => {
let denials: { ability: string; environmentSlug: string }[];
if (val === 'Read Only') {
denials = [
{
ability: 'write',
environmentSlug: slug
}
];
} else if (val === 'No Access') {
denials = [
{
ability: 'write',
environmentSlug: slug
},
{
ability: 'read',
environmentSlug: slug
}
];
} else if (val === 'Add Only') {
denials = [
{
ability: 'read',
environmentSlug: slug
}
];
} else {
denials = [];
}
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
setIsUpgradeModalOpen(true);
} else {
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials});
const allDenials = userData[index].deniedPermissions
.filter(
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
)
.concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
orgId
});
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product)
setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, [userData, myUser]);
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
}
};
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text="You can change user permissions if you switch to Infisical's Professional plan."
/>
<table className="w-full my-0.5">
<thead className="text-gray-400 text-xs font-light">
<table className="my-0.5 w-full">
<thead className="text-xs font-light text-gray-400">
<tr>
<th className="text-left pl-4 py-3.5">NAME</th>
<th className="text-left pl-4 py-3.5">EMAIL</th>
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
{workspaceEnvs.map(env => (
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
<span>{env.slug.toUpperCase()}<br/></span>
<th className="py-3.5 pl-4 text-left">NAME</th>
<th className="py-3.5 pl-4 text-left">EMAIL</th>
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
{workspaceEnvs.map((env) => (
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
<span>
{env.slug.toUpperCase()}
<br />
</span>
{/* <span>PERMISSION</span> */}
</th>
))}
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)
.map((row, index) => (
<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}
</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}
</td>
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
<div className="justify-start h-full flex flex-row items-center">
<Select
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
<div className="flex h-full flex-row items-center justify-start">
<Select
className="w-36 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
// open={isOpen}
onValueChange={(e) => handleRoleUpdate(index, e)}
value={row.role}
disabled={myRole !== 'admin' || myUser === row.email}
isDisabled={myRole !== 'admin' || myUser === row.email}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</Select>
{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
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)}
</div>
</td>
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen}
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
value={
// eslint-disable-next-line no-nested-ternary
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
? "No Access"
// 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)}
{workspaceEnvs.map((env) => (
<td
key={guidGenerator()}
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
>
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
</Select>
</td>)}
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen}
onValueChange={(val) =>
handlePermissionUpdate(index, val, row.membershipId, env.slug)
}
value={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? 'No Access'
: // 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 &&
// row.role !== "admin" &&
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
onButtonPressed={() => handleDelete(row.membershipId, index)}
color="red"
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
/>
</div>
) : (
<div className="w-9 h-9" />
<div className="h-9 w-9" />
)}
</td>
</tr>

View File

@@ -99,8 +99,8 @@ const DashboardInputField = ({
/>
</div>
)}
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
overrideEnabled ? 'visible' : 'invisible group-hover:visible'
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
<button type="button" onClick={() => {
if (modifyValueOverride) {

View File

@@ -175,7 +175,7 @@ const DropZone = ({
id="fileSelect"
type="file"
className="opacity-0 absolute w-full h-full"
accept=".txt,.env,.yml"
accept=""
onChange={handleFileSelect}
/>
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}

View File

@@ -55,7 +55,7 @@ const colorsText = [
/**
* This component represent a single row for an environemnt variable on the dashboard
* @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.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
@@ -108,7 +108,7 @@ const KeyPair = ({
isCapitalized = {isCapitalized}
onChangeHandler={modifyKey}
type="varName"
id={keyPair.id}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.key}
isDuplicate={isDuplicate}
overrideEnabled={keyPair.valueOverride !== undefined}
@@ -124,7 +124,7 @@ const KeyPair = ({
<DashboardInputField
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
type="value"
id={keyPair.id}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
overrideEnabled={keyPair.valueOverride !== undefined}
@@ -137,7 +137,7 @@ const KeyPair = ({
<DashboardInputField
onChangeHandler={modifyComment}
type="comment"
id={keyPair.id}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.comment}
isDuplicate={isDuplicate}
isSideBarOpen={keyPair.id === sidebarSecretId}
@@ -153,7 +153,7 @@ const KeyPair = ({
</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

View File

@@ -60,6 +60,7 @@ export default function MFAStep({
}): JSX.Element {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingResend, setIsLoadingResend] = useState(false);
const [mfaCode, setMfaCode] = useState('');
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
@@ -103,9 +104,12 @@ export default function MFAStep({
const handleResendMfaCode = async () => {
try {
setIsLoadingResend(true);
await sendMfaToken.mutateAsync({ email });
setIsLoadingResend(false);
} catch (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>
<u
className={`font-normal ${
isLoading
isLoadingResend
? 'text-bunker-400'
: 'text-primary-700 hover:text-primary duration-200'
}`}
>
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
{isLoading
{isLoadingResend
? t('mfa:step2-resend-progress')
: t('mfa:step2-resend-submit')}
</button>

View File

@@ -92,7 +92,7 @@ export default function Navbar() {
};
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="flex flex-row items-center">
<div className="flex justify-center py-4">
@@ -165,7 +165,7 @@ export default function Navbar() {
leaveFrom="transform opacity-100 scale-100"
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="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t('nav:user.signed-in-as')}

View File

@@ -13,8 +13,6 @@ import { deriveArgonKey } from './crypto';
const clientOldPassword = 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)
* @param {*} email
@@ -105,9 +103,8 @@ const changePassword = async (
secret: Buffer.from(derivedKey.hash)
});
let res;
try {
res = await changePassword2({
await changePassword2({
clientProof,
protectedKey,
protectedKeyIV,
@@ -118,23 +115,19 @@ const changePassword = async (
salt: result.salt,
verifier: result.verifier
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
tag: encryptedPrivateKeyTag
});
if (res && res.status === 400) {
setCurrentPasswordError(true);
} else if (res && res.status === 200) {
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
}
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
} catch (error) {
setCurrentPasswordError(true);
console.log(error);

View File

@@ -3,9 +3,9 @@ interface Props {
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
privateKey?: string;
}
@@ -20,38 +20,46 @@ export const saveTokenToLocalStorage = ({
privateKey,
}: Props) => {
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) {
localStorage.removeItem("protectedKey");
localStorage.setItem("protectedKey", protectedKey);
}
if (protectedKeyIV) {
localStorage.removeItem("protectedKeyIV");
localStorage.setItem("protectedKeyIV", protectedKeyIV);
}
if (protectedKeyTag) {
localStorage.removeItem("protectedKeyTag");
localStorage.setItem("protectedKeyTag", protectedKeyTag);
}
if (publicKey) {
localStorage.removeItem("publicKey");
localStorage.setItem("publicKey", publicKey);
}
if (privateKey) {
localStorage.setItem("PRIVATE_KEY", privateKey);
if (encryptedPrivateKey) {
localStorage.removeItem("encryptedPrivateKey");
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
}
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
if (iv) {
localStorage.removeItem("iv");
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) {
if (err instanceof Error) {
throw new Error(

View File

@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
export type CardTitleProps = {
children: ReactNode;
subTitle?: string;
subTitle?: ReactNode;
className?: string;
};
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}
{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>
);

View File

@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
import { Button } from '../Button';
import { FormControl } from '../FormControl';
import { Input } from '../Input';
import { Modal, ModalContent } from '../Modal';
import { Modal, ModalClose, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
>
Delete
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</ModalClose>{' '}
</div>
}
onClose={onClose}

View File

@@ -12,7 +12,7 @@ type Props = {
};
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: {
size: {
@@ -84,19 +84,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
): JSX.Element => {
return (
<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
{...props}
required={isRequired}
ref={ref}
disabled={isDisabled}
className={twMerge(
leftIcon ? 'pl-9' : 'pl-2.5',
rightIcon ? 'pr-9' : 'pr-2.5',
leftIcon ? 'pl-10' : 'pl-2.5',
rightIcon ? 'pr-10' : 'pr-2.5',
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>
);
}

View File

@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
subTitle?: ReactNode;
footerContent?: ReactNode;
onClose?: () => void;
};

View File

@@ -14,18 +14,28 @@ type Props = {
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
isDisabled?: boolean;
icon?: IconProp;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export type SelectProps = Omit<SelectPrimitive.SelectProps, 'disabled'> & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
{
children,
placeholder,
className,
isLoading,
isDisabled,
dropdownContainerClassName,
position,
...props
},
ref
): JSX.Element => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Root {...props} disabled={isDisabled}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
@@ -34,10 +44,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
className
)}
>
<SelectPrimitive.Value placeholder={placeholder}>
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
{!props.disabled && (
{!isDisabled && (
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>
@@ -46,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
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
)}
position={position}
@@ -89,11 +99,12 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
<SelectPrimitive.Item
{...props}
className={twMerge(
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
`relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
isDisabled &&
'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
className
)}
ref={forwardedRef}

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

View File

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

View File

@@ -12,5 +12,6 @@ export * from './Select';
export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './Tag';
export * from './TextArea';
export * from './UpgradePlanModal';

View File

@@ -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) => ({
id: encryptedSecretVersion._id,
pos,
@@ -125,10 +127,13 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
);
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(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].id,
@@ -146,6 +151,24 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
valueOverride: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
)[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({
@@ -161,7 +184,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<div
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
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 ? (
<div className="flex items-center justify-center h-full mb-8">

View File

@@ -0,0 +1,5 @@
export {
useAddIncidentContact,
useDeleteIncidentContact,
useGetOrgIncidentContact
} from './queries';

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
export * from './auth';
export * from './incidentContacts';
export * from './keys';
export * from './organization';
export * from './serviceTokens';

View File

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

View File

@@ -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 { Organization } from './types';
import { Organization, RenameOrgDTO } from './types';
const organizationKeys = {
getUserOrganization: ['organization'] as const
@@ -16,3 +16,16 @@ const fetchUserOrganization = async () => {
export const useGetOrganization = () =>
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);
}
});
};

View File

@@ -4,3 +4,8 @@ export type Organization = {
createAt: string;
updatedAt: string;
};
export type RenameOrgDTO = {
orgId: string;
newOrgName: string;
};

View File

@@ -1,14 +1,17 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { IncidentContact } from './incidentContacts/types';
export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
export type { User } from './users/types';
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
export type {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv,

View File

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

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
decryptAssymmetric,
@@ -8,7 +8,15 @@ import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { useUploadWsKey } from '../keys/queries';
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
import {
AddUserToOrgDTO,
AddUserToWsDTO,
AddUserToWsRes,
DeletOrgMembershipDTO,
OrgUser,
UpdateOrgUserRoleDTO,
User
} from './types';
const userKeys = {
getUser: ['user'] as const,
@@ -32,7 +40,11 @@ export const fetchOrgUsers = async (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
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 = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),

View File

@@ -32,7 +32,7 @@ export type OrgUser = {
inviteEmail: string;
organization: string;
role: 'owner' | 'admin' | 'member';
status: 'invited' | 'accepted';
status: 'invited' | 'accepted' | 'verified' | 'completed';
deniedPermissions: any[];
};
@@ -45,3 +45,19 @@ export type AddUserToWsRes = {
invitee: OrgUser['user'];
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;
};

View File

@@ -3,9 +3,9 @@ export {
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment} from './queries';

View File

@@ -13,16 +13,18 @@ import {
Workspace
} from './types';
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
getAllUserWorkspace: ['workspaces'] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
return data.workspace;
}
const { data } = await apiRequest.get<{ workspace: Workspace }>(
`/api/v1/workspace/${workspaceId}`
);
return data.workspace;
};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
@@ -40,6 +42,21 @@ export const useGetWorkspaceById = (workspaceId: string) => {
export const useGetUserWorkspaces = () =>
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
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
@@ -70,7 +87,9 @@ export const useToggleAutoCapitalization = () => {
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
mutationFn: ({ workspaceID, state }) =>
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, {
autoCapitalization: state
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}

View File

@@ -227,7 +227,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Select
defaultValue={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) => {
router.push(`/dashboard/${value}`);
}}

View File

@@ -1,4 +1,4 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
import { apiRequest } from "@app/config/request";
interface Props {
clientProof: string;
@@ -17,7 +17,7 @@ interface Props {
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = ({
const changePassword2 = async ({
clientProof,
protectedKey,
protectedKeyIV,
@@ -27,29 +27,20 @@ const changePassword2 = ({
encryptedPrivateKeyTag,
salt,
verifier
}: Props) =>
SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log('Failed to change the password');
return undefined;
}: Props) => {
const { data } = await apiRequest.post('/api/v1/password/change-password', {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
return data;
}
export default changePassword2;

View File

@@ -167,7 +167,7 @@ export default function Dashboard() {
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -217,6 +217,9 @@ export default function Dashboard() {
useEffect(() => {
(async () => {
if (router.isReady && workspaceId === 'undefined') {
router.push('/noprojects');
}
try {
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
workspaceId
@@ -314,7 +317,7 @@ export default function Dashboard() {
valueOverride: undefined,
comment: '',
tags: []
},
}
]);
};
@@ -332,27 +335,27 @@ export default function Dashboard() {
};
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);
};
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);
};
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);
};
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);
};
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);
};
@@ -444,7 +447,8 @@ export default function Dashboard() {
initialData!
.filter(
(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 !==
initDataPoint.value ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
@@ -477,7 +481,7 @@ export default function Dashboard() {
const overridesToBeAdded = newOverrides!
.filter(
(newDataPoint) =>
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
!initOverrides.map((initDataPoint) => initDataPoint.idOverride).includes(newDataPoint.idOverride)
)
.map((override) => ({
pos: override.pos,
@@ -496,18 +500,21 @@ export default function Dashboard() {
initOverrides
.filter(
(initDataPoint) =>
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
newOverrides!.map((dataPoint) => dataPoint.idOverride)
.includes(initDataPoint.idOverride) &&
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
.valueOverride !== initDataPoint.valueOverride ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment ||
JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
JSON.stringify(initDataPoint?.tags))
newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0].key !==
initDataPoint.key ||
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
.comment || '') !== (initDataPoint.comment || '')
||
(JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]?.tags) || '') !==
(JSON.stringify(initDataPoint?.tags) || '')
)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
.map((secret) => secret.idOverride)
.includes(newDataPoint.idOverride)
)
.map((override) => ({
pos: override.pos,
@@ -665,7 +672,7 @@ export default function Dashboard() {
idOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.id,
pos: (newData?.filter(dp => !dp.id.includes('-'))?.length ?? 0) + index,
pos: (newData?.filter(dp => !dp.id?.includes('-'))?.length ?? 0) + index,
key,
value: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
@@ -681,8 +688,8 @@ export default function Dashboard() {
)[0]?.tags
}));
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))))
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))))
} else {
setInitialData(structuredClone(newData));
}
@@ -746,11 +753,13 @@ export default function Dashboard() {
};
const handleOnEnvironmentChange = (envName: string) => {
if(hasUnsavedChanges) {
if (hasUnsavedChanges) {
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',
slug: 'unknown',
isWriteDenied: false,
@@ -839,16 +848,18 @@ export default function Dashboard() {
</div>
<div className="flex flex-row">
<div className="flex justify-start max-w-sm mt-1 mr-2">
{!selectedEnv?.isReadDenied && <Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
/>}
{!selectedEnv?.isReadDenied && (
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true);
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
/>
)}
</div>
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1">
@@ -882,17 +893,24 @@ export default function Dashboard() {
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version });
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
type: 'success'
});
setHasUnsavedChanges(false);
const result = await performSecretRollback({ workspaceId, version: snapshotData.version });
if (result === undefined) {
createNotification({
text: `Something went wrong during the rollback.`,
type: 'error'
});
} else {
setData(rolledBackSecrets);
createNotification({
text: `Successfully rolled back secrets.`,
type: 'success'
});
setSnapshotData(undefined);
setHasUnsavedChanges(false);
togglePITSidebar(false);
}
}}
color="primary"
size="md"
@@ -975,57 +993,68 @@ export default function Dashboard() {
alt="infisical loading indicator"
/>
</div>
) : data?.length !== 0 ? (
) : (data?.length !== 0 || snapshotData?.secretVersions) ? (
<div className="flex flex-col w-full mt-1">
<div
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"
>
<div ref={secretsTop} />
<div
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="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="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>
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
<div className="text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default">
0
</div>
<span className="px-2 text-bunker-300 font-semibold">Key</span>
{!snapshotData && (
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? (
<FontAwesomeIcon icon={faArrowUp} />
) : (
<FontAwesomeIcon icon={faArrowDown} />
)}
</IconButton>
)}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
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>
<div 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>
</div>
</div>
<div className="w-[calc(10%)] border-r border-mineshaft-600">
<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 className="w-2/12">
<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
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] 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
onKeyDown={() => null}
role="none"
onClick={() => {}}
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>
@@ -1033,12 +1062,18 @@ export default function Dashboard() {
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData &&
data
?.filter((row) =>
row.key?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
?.filter(
(row) =>
row.key?.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) => row.value !== undefined)
// .filter((row) => row.value !== undefined)
.map((keyPair) => (
<KeyPair
isCapitalized={autoCapitalization}
@@ -1066,22 +1101,6 @@ export default function Dashboard() {
?.sort((a, b) => a.key.localeCompare(b.key))
.filter((row) => row.environment === selectedSnapshotEnv?.slug)
.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) => (
<KeyPair
isCapitalized={autoCapitalization}
@@ -1102,15 +1121,15 @@ export default function Dashboard() {
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='w-10'/>
<button
<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" />
<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}
>
<FontAwesomeIcon icon={faPlus} className='mr-3'/>
<span className='text-sm'>Add Secret</span>
<FontAwesomeIcon icon={faPlus} className="mr-3" />
<span className="text-sm">Add Secret</span>
</button>
</div>
</div>
@@ -1157,8 +1176,7 @@ export default function Dashboard() {
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.id === sidebarSecretId && row.value !== undefined
(row: SecretDataProps) => row.id === sidebarSecretId && row.value !== undefined
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}

View File

@@ -1,374 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
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 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';
import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
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();
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 (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<>
<Head>
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-row">
<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>
<OrgSettingsPage />
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
export { OrgMembersTable } from './OrgMembersTable';
export { OrgNameChangeSection } from './OrgNameChangeSection';

View File

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

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { Checkbox } from '@app/components/v2';
import { useGetUser } from '../../../../hooks/api';
@@ -9,6 +10,7 @@ import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
export const SecuritySection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
const { createNotification } = useNotificationContext();
useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') {
@@ -21,18 +23,27 @@ export const SecuritySection = () => {
const newUser: User = await updateMyMfaEnabled({
isMfaEnabled: state
});
if (newUser) {
setIsMfaEnabled(newUser.isMfaEnabled);
}
createNotification({
text: `${newUser.isMfaEnabled ? 'Successfully turned on two-factor authentication.' : 'Successfully turned off two-factor authentication.'}`,
type: 'success'
});
} catch (err) {
createNotification({
text: 'Something went wrong while toggling the two-factor authentication.',
type: 'error'
});
console.error(err);
}
}
return (
<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">
Two-factor Authentication
</p>

View File

@@ -58,7 +58,7 @@ export const ProjectSettingsPage = () => {
const renameWorkspace = useRenameWorkspace();
const toggleAutoCapitalization = useToggleAutoCapitalization();
const deleteWorkspace = useDeleteWorkspace();
// env crud operation
const createWsEnv = useCreateWsEnvironment();
@@ -97,7 +97,7 @@ export const ProjectSettingsPage = () => {
}
};
const onAutoCapitalizationToggle = async (state: boolean) => {
const onAutoCapitalizationToggle = async (state: boolean) => {
try {
await toggleAutoCapitalization.mutateAsync({
workspaceID,
@@ -123,6 +123,9 @@ export const ProjectSettingsPage = () => {
await deleteWorkspace.mutateAsync({ workspaceID });
// redirect user to first workspace user is part of
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
if (!ws) {
router.push('/noprojects');
}
router.push(`/dashboard/${ws?._id}`);
createNotification({
text: 'Successfully deleted workspace',
@@ -247,7 +250,7 @@ export const ProjectSettingsPage = () => {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
tagSlug: name.replace(' ', '_')
});
createNotification({
text: 'Successfully created a tag',

View File

@@ -7,7 +7,7 @@ type: application
# 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.
# 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
# incremented each time you make changes to the application. Versions are not expected to
@@ -23,4 +23,4 @@ dependencies:
- name: mailhog
version: "~5.2.3"
repository: "https://codecentric.github.io/helm-charts"
condition: mailhog.enabled
condition: mailhog.enabled

View File

@@ -209,6 +209,8 @@ mongodb:
##
databases:
- "infisical"
rootPassword: root
rootUser: root
## MongoDB persistence configuration
##
persistence:
@@ -250,7 +252,7 @@ ingress:
kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
## @param ingress.hostName Ingress hostname (your custom domain name)
## Replace with your own domain
## Replace with your own domain
##
hostName: infisical.local
## @skip ingress.frontend
@@ -264,9 +266,10 @@ ingress:
path: /api
pathType: Prefix
## @param ingress.tls Ingress TLS hosts (matching above hostName)
## Replace with your own domain
## Replace with your own domain
##
tls: []
tls:
[]
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
@@ -338,7 +341,8 @@ mailhog:
ingressClassName: nginx
## @param mailhog.ingress.annotations Ingress annotations
##
annotations: {}
annotations:
{}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
## @param mailhog.ingress.labels Ingress labels
@@ -352,4 +356,4 @@ mailhog:
##
paths:
- path: "/"
pathType: Prefix
pathType: Prefix