mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge remote-tracking branch 'origin' into mfa
This commit is contained in:
6
.github/values.yaml
vendored
6
.github/values.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface SecretDataProps {
|
||||
value: string | undefined;
|
||||
valueOverride: string | undefined;
|
||||
id: string;
|
||||
idOverride?: string;
|
||||
comment: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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="" />}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
|
||||
|
||||
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: string;
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
24
frontend/src/components/v2/Tag/Tag.tsx
Normal file
24
frontend/src/components/v2/Tag/Tag.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { cva, VariantProps } from 'cva';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
} & VariantProps<typeof tagVariants>;
|
||||
|
||||
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 ', {
|
||||
variants: {
|
||||
colorSchema: {
|
||||
gray: 'bg-mineshaft-500',
|
||||
red: 'bg-red/80 text-bunker-100'
|
||||
},
|
||||
size: {
|
||||
sm: 'px-1 py-0.5'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
|
||||
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
|
||||
);
|
||||
1
frontend/src/components/v2/Tag/index.tsx
Normal file
1
frontend/src/components/v2/Tag/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { Tag } from './Tag';
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
5
frontend/src/hooks/api/incidentContacts/index.tsx
Normal file
5
frontend/src/hooks/api/incidentContacts/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
useAddIncidentContact,
|
||||
useDeleteIncidentContact,
|
||||
useGetOrgIncidentContact
|
||||
} from './queries';
|
||||
57
frontend/src/hooks/api/incidentContacts/queries.tsx
Normal file
57
frontend/src/hooks/api/incidentContacts/queries.tsx
Normal 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));
|
||||
}
|
||||
});
|
||||
};
|
||||
18
frontend/src/hooks/api/incidentContacts/types.ts
Normal file
18
frontend/src/hooks/api/incidentContacts/types.ts
Normal 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;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './incidentContacts';
|
||||
export * from './keys';
|
||||
export * from './organization';
|
||||
export * from './serviceTokens';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { useGetOrganization } from './queries';
|
||||
export { useGetOrganization, useRenameOrg } from './queries';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,3 +4,8 @@ export type Organization = {
|
||||
createAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type RenameOrgDTO = {
|
||||
orgId: string;
|
||||
newOrgName: string;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToOrg,
|
||||
useAddUserToWs,
|
||||
useDeleteOrgMembership,
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useLogoutUser
|
||||
useLogoutUser,
|
||||
useUpdateOrgUserRole
|
||||
} from './queries';
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ export {
|
||||
useCreateWsEnvironment,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetWorkspaceById,
|
||||
useRenameWorkspace,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateWsEnvironment} from './queries';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
308
frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
Normal file
308
frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import { IncidentContact } from '@app/hooks/api/types';
|
||||
|
||||
type Props = {
|
||||
contacts?: IncidentContact[];
|
||||
onRemoveContact: (email: string) => Promise<void>;
|
||||
onAddContact: (email: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const addContactFormSchema = yup.object({
|
||||
email: yup.string().email().required().label('Email').trim()
|
||||
});
|
||||
|
||||
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||
|
||||
export const OrgIncidentContactsTable = ({
|
||||
contacts = [],
|
||||
onAddContact,
|
||||
onRemoveContact
|
||||
}: Props) => {
|
||||
const [searchContact, setSearchContact] = useState('');
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'addContact',
|
||||
'removeContact'
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||
|
||||
const onAddIncidentContact = ({ email }: TAddContactForm) => {
|
||||
onAddContact(email);
|
||||
handlePopUpClose('addContact');
|
||||
reset();
|
||||
};
|
||||
|
||||
const onRemoveIncidentContact = async () => {
|
||||
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||
await onRemoveContact(incidentContactEmail);
|
||||
handlePopUpClose('removeContact');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
value={searchContact}
|
||||
onChange={(e) => setSearchContact(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search incident contact by email..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen('addContact')}
|
||||
>
|
||||
Add Contact
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Email</Th>
|
||||
<Th aria-label="actions" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{contacts
|
||||
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||
?.map(({ email }) => (
|
||||
<Tr key={email}>
|
||||
<Td className="w-full">{email}</Td>
|
||||
<Td className="mr-4">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen('removeContact', { email })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{contacts
|
||||
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||
?.length === 0 && (
|
||||
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.addContact?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('addContact', isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add an Incident Contact"
|
||||
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onAddIncidentContact)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add Incident Contact
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose('addContact')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeContact.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this email from incident contact?"
|
||||
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
|
||||
onDeleteApproved={onRemoveIncidentContact}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import { OrgUser, Workspace } from '@app/hooks/api/types';
|
||||
|
||||
type Props = {
|
||||
members?: OrgUser[];
|
||||
workspaceMemberships?: Record<string, Workspace[]>;
|
||||
orgName: string;
|
||||
isMoreUserNotAllowed: boolean;
|
||||
onRemoveMember: (userId: string) => Promise<void>;
|
||||
onInviteMember: (email: string) => Promise<void>;
|
||||
onRoleChange: (membershipId: string, role: string) => Promise<void>;
|
||||
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
||||
// the current user id to block remove org button
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const addMemberFormSchema = yup.object({
|
||||
email: yup.string().email().required().label('Email').trim()
|
||||
});
|
||||
|
||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||
|
||||
export const OrgMembersTable = ({
|
||||
members = [],
|
||||
workspaceMemberships = {},
|
||||
orgName,
|
||||
isMoreUserNotAllowed,
|
||||
onRemoveMember,
|
||||
onInviteMember,
|
||||
onGrantAccess,
|
||||
onRoleChange,
|
||||
userId
|
||||
}: Props) => {
|
||||
const [searchMemberFilter, setSearchMemberFilter] = useState('');
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'addMember',
|
||||
'removeMember',
|
||||
'upgradePlan'
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||
|
||||
const onAddMember = ({ email }: TAddMemberForm) => {
|
||||
onInviteMember(email);
|
||||
handlePopUpClose('addMember');
|
||||
reset();
|
||||
};
|
||||
|
||||
const onRemoveOrgMemberApproved = async () => {
|
||||
const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||
await onRemoveMember(orgMembershipId);
|
||||
handlePopUpClose('removeMember');
|
||||
};
|
||||
|
||||
const isIamOwner = useMemo(
|
||||
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
|
||||
[userId, members]
|
||||
);
|
||||
|
||||
const filterdUser = useMemo(
|
||||
() =>
|
||||
members.filter(
|
||||
({ user, inviteEmail }) =>
|
||||
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||
inviteEmail?.includes(searchMemberFilter)
|
||||
),
|
||||
[members, searchMemberFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
value={searchMemberFilter}
|
||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreUserNotAllowed) {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
} else {
|
||||
handlePopUpOpen('addMember');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Email</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Projects</Th>
|
||||
<Th aria-label="actions" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||
const name = user ? `${user.firstName} ${user.lastName}` : '-';
|
||||
const email = user?.email || inviteEmail;
|
||||
const userWs = workspaceMemberships?.[user?._id];
|
||||
|
||||
return (
|
||||
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
||||
<Td>{name}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
{status === 'accepted' && (
|
||||
<Select
|
||||
defaultValue={role}
|
||||
isDisabled={userId === user?._id}
|
||||
className="w-full bg-mineshaft-600"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{(isIamOwner || role === 'owner') && (
|
||||
<SelectItem value="owner">owner</SelectItem>
|
||||
)}
|
||||
<SelectItem value="admin">admin</SelectItem>
|
||||
<SelectItem value="member">member</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
{(status === 'invited' || status === 'verified') && (
|
||||
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
|
||||
Resent Invite
|
||||
</Button>
|
||||
)}
|
||||
{status === 'completed' && (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
||||
>
|
||||
Grant Access
|
||||
</Button>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{userWs ? (
|
||||
userWs?.map(({ name: wsName, _id }) => (
|
||||
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
||||
{wsName}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Tag colorSchema="red">This user isn't part of any projects yet</Tag>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== user?._id && <IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
isDisabled={userId === user?._id}
|
||||
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.addMember?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('addMember', isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={`Invite others to ${orgName}`}
|
||||
subTitle={
|
||||
<>
|
||||
An invite is specific to an email address and expires after 1 day.
|
||||
<br />
|
||||
For security reasons, you will need to separately add members to projects.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onAddMember)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose('addMember')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this user from the org?"
|
||||
onChange={(isOpen) => handlePopUpToggle('removeMember', isOpen)}
|
||||
onDeleteApproved={onRemoveOrgMemberApproved}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgMembersTable } from './OrgMembersTable';
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Button, FormControl, Input } from '@app/components/v2';
|
||||
|
||||
type Props = {
|
||||
orgName?: string;
|
||||
onOrgNameChange: (name: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label('Project Name')
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
reset({ name: orgName });
|
||||
}, [orgName]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
await onOrgNameChange(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-3">
|
||||
<p className="mb-4 mt-2 text-xl font-semibold">{t('common:display-name')}</p>
|
||||
<div className="mb-2 w-full max-w-lg">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Type your org name" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isDisabled={!isDirty || isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||
>
|
||||
{t('common:save-changes')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||
@@ -0,0 +1,3 @@
|
||||
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||
export { OrgMembersTable } from './OrgMembersTable';
|
||||
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||
1
frontend/src/views/Settings/OrgSettingsPage/index.tsx
Normal file
1
frontend/src/views/Settings/OrgSettingsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { OrgSettingsPage } from './OrgSettingsPage';
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user