mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 15:28:42 +00:00
Add frontend, backend and CLI
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import "../styles/globals.css";
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
import { config } from "@fortawesome/fontawesome-svg-core";
|
||||
import Layout from "../components/basic/layout";
|
||||
import RouteGuard from "../components/RouteGuard";
|
||||
import { publicPaths } from "../const.js";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { initPostHog } from "../components/analytics/posthog";
|
||||
|
||||
config.autoAddCss = false;
|
||||
|
||||
const App = ({ Component, pageProps, ...appProps }) => {
|
||||
const router = useRouter();
|
||||
const posthog = initPostHog();
|
||||
|
||||
useEffect(() => {
|
||||
// Init for auto capturing
|
||||
const posthog = initPostHog();
|
||||
|
||||
const handleRouteChange = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
if (process.env.NEXT_PUBLIC_ENV == "production") {
|
||||
posthog.capture("$pageview");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
// If it's one of these routes, don't add the layout (e.g., these routes are external)
|
||||
if (
|
||||
publicPaths.includes("/" + appProps.router.pathname.split("/")[1]) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
return (
|
||||
<Component {...pageProps} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteGuard>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</RouteGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
{
|
||||
/* <Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){window.dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-DQ1XLJJGG1');
|
||||
`}
|
||||
</Script> */
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This is the second step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const changePassword2 = ({encryptedPrivateKey, iv, tag, salt, verifier, clientProof}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/password/change-password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"clientProof": clientProof,
|
||||
"encryptedPrivateKey": encryptedPrivateKey,
|
||||
"iv": iv,
|
||||
"tag": tag,
|
||||
"salt": salt,
|
||||
"verifier": verifier
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to change the password');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default changePassword2;
|
||||
@@ -0,0 +1,27 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient.js";
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This function is used to check if the user is authenticated.
|
||||
* To do that, we get their tokens from cookies, and verify if they are good.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const checkAuth = async (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/auth/checkAuth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log("Not authorized");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default checkAuth;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This route check the verification code from the email that user just recieved
|
||||
* @param {*} email
|
||||
* @param {*} code
|
||||
* @returns
|
||||
*/
|
||||
const checkEmailVerificationCode = (email, code) => {
|
||||
return fetch(PATH + "/api/v1/signup/email/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
code: code,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default checkEmailVerificationCode;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {*} email
|
||||
* @param {*} firstName
|
||||
* @param {*} lastName
|
||||
* @param {*} workspace
|
||||
* @param {*} publicKey
|
||||
* @param {*} ciphertext
|
||||
* @param {*} iv
|
||||
* @param {*} tag
|
||||
* @param {*} salt
|
||||
* @param {*} verifier
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignup = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}) => {
|
||||
return fetch(PATH + "/api/v1/signup/complete-account/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
organizationName,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignup;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {*} email
|
||||
* @param {*} firstName
|
||||
* @param {*} lastName
|
||||
* @param {*} publicKey
|
||||
* @param {*} ciphertext
|
||||
* @param {*} iv
|
||||
* @param {*} tag
|
||||
* @param {*} salt
|
||||
* @param {*} verifier
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignupInvite = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}) => {
|
||||
return fetch(PATH + "/api/v1/signup/complete-account/invite", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
publicKey: publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv: iv,
|
||||
tag: tag,
|
||||
salt: salt,
|
||||
verifier: verifier
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignupInvite;
|
||||
@@ -0,0 +1,32 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This is the route that issues a backup private key that will afterwards be added into a pdf
|
||||
*/
|
||||
const issueBackupPrivateKey = ({encryptedPrivateKey, iv, tag, salt, verifier, clientProof}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/password/backup-private-key", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"clientProof": clientProof,
|
||||
"encryptedPrivateKey": encryptedPrivateKey,
|
||||
"iv": iv,
|
||||
"tag": tag,
|
||||
"salt": salt,
|
||||
"verifier": verifier
|
||||
}),
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
return res;
|
||||
console.log("Failed to issue the backup key");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default issueBackupPrivateKey;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This is the first step of the login process (pake)
|
||||
* @param {*} email
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const login1 = (email, clientPublicKey) => {
|
||||
return fetch(PATH + "/api/v1/auth/login1", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
clientPublicKey,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default login1;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This is the second step of the login process
|
||||
* @param {*} email
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const login2 = (email, clientProof) => {
|
||||
return fetch(PATH + "/api/v1/auth/login2", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
clientProof,
|
||||
}),
|
||||
credentials: "include"
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status == 200) {
|
||||
console.log("User logged in", res);
|
||||
return res;
|
||||
} else {
|
||||
console.log("Failed to log in");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default login2;
|
||||
@@ -0,0 +1,36 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route logs the user out. Note: the user should authorized to do this.
|
||||
* We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const logout = async (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include"
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status == 200) {
|
||||
SecurityClient.setToken("");
|
||||
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
||||
localStorage.setItem("publicKey", "");
|
||||
localStorage.setItem("encryptedPrivateKey", "");
|
||||
localStorage.setItem("iv", "");
|
||||
localStorage.setItem("tag", "");
|
||||
localStorage.setItem("PRIVATE_KEY", "");
|
||||
console.log("User logged out", res);
|
||||
return res;
|
||||
} else {
|
||||
console.log("Failed to log out");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default logout;
|
||||
@@ -0,0 +1,28 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This is the first step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const SRP1 = ({clientPublicKey}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/password/srp1", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientPublicKey,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to do the first step of SRP');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default SRP1;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This route send the verification email to the user's email (contains a 6-digit verification code)
|
||||
* @param {*} email
|
||||
*/
|
||||
const sendVerificationEmail = (email) => {
|
||||
fetch(PATH + "/api/v1/signup/email/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default sendVerificationEmail;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
const token = async (req, res) => {
|
||||
return fetch(PATH + "/api/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include"
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).token;
|
||||
} else {
|
||||
console.log('Getting a new token failed');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default token;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route verifies the signup invite link
|
||||
* @param {*} email
|
||||
* @param {*} code
|
||||
* @returns
|
||||
*/
|
||||
const verifySignupInvite = ({email, code}) => {
|
||||
return fetch(PATH + "/api/v1/invite-org/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default verifySignupInvite;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This route lets us get the public key of infisical. Th euser doesn't have to be authenticated since this is just the public key.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const publicKeyInfisical = (req, res) => {
|
||||
return fetch(PATH + "/api/v1/key/publicKey/infisical", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default publicKeyInfisical;
|
||||
@@ -0,0 +1,33 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient.js";
|
||||
import { PATH } from "../../../const.js";
|
||||
|
||||
/**
|
||||
* This function fetches the encrypted secrets from the .env file
|
||||
* @param {*} workspaceId
|
||||
* @param {*} env
|
||||
* @returns
|
||||
*/
|
||||
const getSecrets = async (workspaceId, env) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/secret/" +
|
||||
workspaceId +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
environment: env,
|
||||
channel: "web",
|
||||
}
|
||||
), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to get project secrets');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getSecrets;
|
||||
@@ -0,0 +1,32 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function uploads the encrypted .env file
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const uploadSecrets = async ({ workspaceId, secrets, keys, environment }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/secret/" + workspaceId, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secrets,
|
||||
keys,
|
||||
environment,
|
||||
channel: "web",
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to push secrets');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default uploadSecrets;
|
||||
@@ -0,0 +1,24 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
const changeHerokuConfigVars = ({ integrationId, key, secrets }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration/" + integrationId + "/sync", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key,
|
||||
secrets,
|
||||
})
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to sync secrets to Heroku');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default changeHerokuConfigVars;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route deletes an integration from a certain project
|
||||
* @param {*} integrationId
|
||||
* @returns
|
||||
*/
|
||||
const deleteIntegration = ({ integrationId }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration/" + integrationId, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspace;
|
||||
} else {
|
||||
console.log('Failed to delete an integration');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteIntegration;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route deletes an integration authorization from a certain project
|
||||
* @param {*} integrationAuthId
|
||||
* @returns
|
||||
*/
|
||||
const deleteIntegrationAuth = ({ integrationAuthId }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration-auth/" + integrationAuthId, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to delete an integration authorization');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteIntegrationAuth;
|
||||
@@ -0,0 +1,20 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
const getIntegrationApps = ({ integrationAuthId }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration-auth/" + integrationAuthId + "/apps", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).apps;
|
||||
} else {
|
||||
console.log('Failed to get available apps for an integration');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getIntegrationApps;
|
||||
@@ -0,0 +1,20 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
const getIntegrations = () => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration/integrations", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).integrations;
|
||||
} else {
|
||||
console.log('Failed to get project integrations');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getIntegrations;
|
||||
@@ -0,0 +1,32 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route starts the integration after teh default one if gonna set up.
|
||||
* @param {*} integrationId
|
||||
* @returns
|
||||
*/
|
||||
const startIntegration = ({ integrationId, appName, environment }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration/" + integrationId, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"update": {
|
||||
app: appName,
|
||||
environment,
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to start an integration');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default startIntegration;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This is the first step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const AuthorizeIntegration = ({ workspaceId, code, integration }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/integration-auth/oauth-token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
code,
|
||||
integration
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to authorize the integration');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default AuthorizeIntegration;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route gets authorizations of a certain project (Heroku, etc.)
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceAuthorizations = ({ workspaceId }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId + "/authorizations", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).authorizations;
|
||||
} else {
|
||||
console.log('Failed to get project authorizations');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaceAuthorizations;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route gets integrations of a certain project (Heroku, etc.)
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceIntegrations = ({ workspaceId }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId + "/integrations", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).integrations;
|
||||
} else {
|
||||
console.log('Failed to get the project integrations');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaceIntegrations;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get info about a certain org
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganization = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + req.orgId, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).organization;
|
||||
} else {
|
||||
console.log('Failed to get org info');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganization;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get all the users in an org.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganizationProjects = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/organization/" + req.orgId + "/workspaces", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspaces;
|
||||
} else {
|
||||
console.log('Failed to get projects for an org');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganizationProjects;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get the current subscription of an org.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganizationSubscriptions = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + req.orgId + "/subscriptions", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).subscriptions;
|
||||
} else {
|
||||
console.log('Failed to get org subscriptions');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganizationSubscriptions;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get all the projects of a certain user in an org.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganizationUserProjects = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + req.orgId + "/my-workspaces", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspaces;
|
||||
} else {
|
||||
console.log('Failed to get projects of a user in an org');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganizationUserProjects;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get all the users in an org.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganizationUsers = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + req.orgId + "/users", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).users;
|
||||
} else {
|
||||
console.log('Failed to get org users');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganizationUsers;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route redirects the user to the right stripe billing page.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const StripeRedirect = ({orgId}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + orgId + "/customer-portal-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return window.location.href = (await res.json()).url;
|
||||
} else {
|
||||
console.log('Failed to redirect to Stripe');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default StripeRedirect;
|
||||
@@ -0,0 +1,28 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route add an incident contact email to a certain organization
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const addIncidentContact = (organizationId, email) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + organizationId + "/incidentContactOrg", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to add an incident contact');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default addIncidentContact;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function sends an email invite to a user to join an org
|
||||
* @param {*} email
|
||||
* @param {*} orgId
|
||||
* @returns
|
||||
*/
|
||||
const addUserToOrg = (email, orgId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/invite-org/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
inviteeEmail: email,
|
||||
organizationId: orgId,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to add a user to an org');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default addUserToOrg;
|
||||
@@ -0,0 +1,28 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route deletes an incident Contact from a certain organization
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const deleteIncidentContact = (organizaionId, email) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + organizaionId + "/incidentContactOrg", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to delete an incident contact');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteIncidentContact;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function removes a certain member from a certain organization
|
||||
* @param {*} membershipId
|
||||
* @returns
|
||||
*/
|
||||
const deleteUserFromOrganization = (membershipId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/membership-org/" + membershipId, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to delete a user from an org');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteUserFromOrganization;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This routes gets all the incident contacts of a certain organization
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getIncidentContacts = (organizationId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + organizationId + "/incidentContactOrg", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).incidentContactsOrg;
|
||||
} else {
|
||||
console.log('Failed to get incident contacts');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getIncidentContacts;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get the all the orgs of a certain user.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getOrganizations = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).organizations;
|
||||
} else {
|
||||
console.log('Failed to get orgs of a user');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getOrganizations;
|
||||
@@ -0,0 +1,29 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us rename a certain org.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const renameOrg = (orgId, newOrgName) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/organization/" + orgId + "/name", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newOrgName,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to rename an organization');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default renameOrg;
|
||||
@@ -0,0 +1,34 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route gets service tokens for a specific user in a project
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const addServiceToken = ({name, workspaceId, environment, expiresIn, publicKey, encryptedKey, nonce}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/service-token/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
})
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).token;
|
||||
} else {
|
||||
console.log('Failed to add service tokens');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default addServiceToken;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route gets service tokens for a specific user in a project
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const getServiceTokens = ({workspaceId}) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId + "/service-tokens", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).serviceTokens;
|
||||
} else {
|
||||
console.log('Failed to get service tokens');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getServiceTokens;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route gets the information about a specific user.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getUser = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/user", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).user;
|
||||
} else {
|
||||
console.log('Failed to get user info');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getUser;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route registers a certain action for a user
|
||||
* @param {*} email
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const checkUserAction = ({ action }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/user-action" +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
action,
|
||||
}), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).userAction;
|
||||
} else {
|
||||
console.log('Failed to check a user action');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default checkUserAction;
|
||||
@@ -0,0 +1,28 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route registers a certain action for a user
|
||||
* @param {*} action
|
||||
* @returns
|
||||
*/
|
||||
const registerUserAction = ({ action }) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/user-action", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to register a user action');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default registerUserAction;
|
||||
@@ -0,0 +1,29 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function adds a user to a project
|
||||
* @param {*} email
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const addUserToWorkspace = (email, workspaceId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId + "/invite-signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to add a user to project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default addUserToWorkspace;
|
||||
@@ -0,0 +1,29 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function change the access of a user in a certain workspace
|
||||
* @param {*} membershipId
|
||||
* @param {*} role
|
||||
* @returns
|
||||
*/
|
||||
const changeUserRoleInWorkspace = (membershipId, role) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/membership/" + membershipId + "/change-role", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: role,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to change the user role in a project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default changeUserRoleInWorkspace;
|
||||
@@ -0,0 +1,29 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route creates a new workspace for a user.
|
||||
* @param {*} workspaceName
|
||||
* @returns
|
||||
*/
|
||||
const createWorkspace = (workspaceName, organizationId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceName: workspaceName,
|
||||
organizationId: organizationId,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspace;
|
||||
} else {
|
||||
console.log('Failed to create a project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default createWorkspace;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This function removes a certain member from a certain workspace
|
||||
* @param {*} membershipId
|
||||
* @returns
|
||||
*/
|
||||
const deleteUserFromWorkspace = (membershipId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/membership/" + membershipId, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to delete a user from a project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteUserFromWorkspace;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route deletes a specified workspace.
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const deleteWorkspace = (workspaceId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to delete a project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default deleteWorkspace;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* Get the latest key pairs from a certain workspace
|
||||
* @param {*} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getLatestFileKey = (workspaceId) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/key/" + workspaceId + "/latest", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to get the latest key pairs for a certain project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getLatestFileKey;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get the information of a certain project.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceInfo = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + req.workspaceId, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspace;
|
||||
} else {
|
||||
console.log('Failed to get project info');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaceInfo;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get the public keys of everyone in your workspace.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceKeys = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + req.workspaceId + "/keys", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).publicKeys;
|
||||
} else {
|
||||
console.log('Failed to get the public keys of everyone in the workspace');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaceKeys;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get all the users in the workspace.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceUsers = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + req.workspaceId + "/users", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).users;
|
||||
} else {
|
||||
console.log('Failed to get Project Users');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaceUsers;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us get the public keys of everyone in your workspace.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaces = (req, res) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).workspaces;
|
||||
} else {
|
||||
console.log('Failed to get projects');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default getWorkspaces;
|
||||
@@ -0,0 +1,29 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route lets us rename a certain workspace.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const renameWorkspace = (workspaceId, newWorkspaceName) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/workspace/" + workspaceId + "/name", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newWorkspaceName,
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to rename a project');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default renameWorkspace;
|
||||
@@ -0,0 +1,35 @@
|
||||
import SecurityClient from "../../../components/utilities/SecurityClient";
|
||||
import { PATH } from "../../../const";
|
||||
|
||||
/**
|
||||
* This route uplods the keys in an encrypted format.
|
||||
* @param {*} workspaceId
|
||||
* @param {*} userId
|
||||
* @param {*} encryptedKey
|
||||
* @param {*} nonce
|
||||
* @returns
|
||||
*/
|
||||
const uploadKeys = (workspaceId, userId, encryptedKey, nonce) => {
|
||||
return SecurityClient.fetchCall(PATH + "/api/v1/key/" + workspaceId, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: {
|
||||
userId: userId,
|
||||
encryptedKey: encryptedKey,
|
||||
nonce: nonce,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to upload keys for a new user');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default uploadKeys;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
|
||||
export default function DashboardRedirect() {
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Here we forward to the default workspace if a user opens this url
|
||||
*/
|
||||
useEffect(async () => {
|
||||
let userWorkspace;
|
||||
try {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
|
||||
} else {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0]._id;
|
||||
router.push("/dashboard/" + userWorkspace);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
DashboardRedirect.requireAuth = true;
|
||||
@@ -0,0 +1,686 @@
|
||||
import React, { useState, useEffect, useCallback, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import guidGenerator from "../../components/utilities/randomId";
|
||||
import getSecretsForProject from "../../components/utilities/getSecretsForProject";
|
||||
import pushKeys from "../../components/utilities/pushKeys";
|
||||
import getWorkspaces from "../api/workspace/getWorkspaces";
|
||||
import getUser from "../api/user/getUser";
|
||||
import NavHeader from "../../components/navigation/NavHeader";
|
||||
|
||||
import DashboardInputField from "../../components/dashboard/DashboardInputField";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faMagnifyingGlass,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
faFolderOpen,
|
||||
faArrowDownAZ,
|
||||
faArrowDownZA,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
faPerson,
|
||||
faPeopleGroup,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faCircleInfo,
|
||||
faX
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import ListBox from "../../components/basic/Listbox";
|
||||
import DropZone from "../../components/dashboard/DropZone";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||
import BottonRightPopup from "../../components/basic/popups/BottomRightPopup";
|
||||
import checkUserAction from "../api/userActions/checkUserAction";
|
||||
import registerUserAction from "../api/userActions/registerUserAction";
|
||||
import pushKeysIntegration from "../../components/utilities/pushKeysIntegration";
|
||||
import Button from "../../components/basic/buttons/Button";
|
||||
|
||||
const KeyPair = ({ keyPair, deleteRow, modifyKey, modifyValue, modifyVisibility, isBlurred }) => {
|
||||
return (
|
||||
<div className="px-1 flex flex-col items-center ml-1">
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-10 my-1 items-center px-2">
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex items-center md:px-1 rounded-lg mt-4 md:mt-0 max-h-10">
|
||||
<DashboardInputField
|
||||
label=""
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
index={keyPair[1]}
|
||||
value={keyPair[2]}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
label=""
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
index={keyPair[1]}
|
||||
value={keyPair[3]}
|
||||
placeholder=""
|
||||
isRequired
|
||||
blurred={isBlurred}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="cursor-pointer w-9 h-9 bg-white/10 rounded-md flex flex-row justify-center items-center opacity-50 hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-44 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
onClick={() =>
|
||||
modifyVisibility(keyPair[4] == "personal"
|
||||
? "shared"
|
||||
: "personal", keyPair[1])
|
||||
}
|
||||
className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={
|
||||
keyPair[4] == "personal"
|
||||
? faPeopleGroup
|
||||
: faPerson
|
||||
}
|
||||
/>
|
||||
<div className="text-sm">
|
||||
{keyPair[4] == "personal"
|
||||
? "Make Shared"
|
||||
: "Make Personal"}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<div className="w-2"></div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair[0])}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const envMapping = {
|
||||
Development: "dev",
|
||||
Staging: "staging",
|
||||
Production: "prod",
|
||||
Testing: "test",
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
const [data, setData] = useState();
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useState("");
|
||||
const [blurred, setBlurred] = useState(true);
|
||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||
const [env, setEnv] = useState(
|
||||
router.asPath.split("?").length == 1
|
||||
? "Development"
|
||||
: Object.keys(envMapping).includes(router.asPath.split("?")[1])
|
||||
? router.asPath.split("?")[1]
|
||||
: "Development"
|
||||
);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [searchKeys, setSearchKeys] = useState("");
|
||||
const [errorDragAndDrop, setErrorDragAndDrop] = useState(false);
|
||||
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||
const [sortMethod, setSortMethod] = useState("alphabetical");
|
||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||
|
||||
// #TODO: fix save message for changing reroutes
|
||||
// const beforeRouteHandler = (url) => {
|
||||
// const warningText =
|
||||
// "Do you want to save your results bfore leaving this page?";
|
||||
// if (!buttonReady) return;
|
||||
// if (router.asPath !== url && !confirm(warningText)) {
|
||||
// // router.events.emit('routeChangeError');
|
||||
// // setData(data)
|
||||
// savePush();
|
||||
// throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
|
||||
// } else {
|
||||
// setButtonReady(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// prompt the user if they try and leave with unsaved changes
|
||||
useEffect(() => {
|
||||
const warningText =
|
||||
"Do you want to save your results before leaving this page?";
|
||||
const handleWindowClose = (e) => {
|
||||
if (!buttonReady) return;
|
||||
e.preventDefault();
|
||||
return (e.returnValue = warningText);
|
||||
};
|
||||
window.addEventListener("beforeunload", handleWindowClose);
|
||||
// router.events.on('routeChangeStart', beforeRouteHandler);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleWindowClose);
|
||||
// router.events.off('routeChangeStart', beforeRouteHandler);
|
||||
};
|
||||
}, [buttonReady]);
|
||||
|
||||
|
||||
const reorderRows = () => {
|
||||
setSortMethod(
|
||||
sortMethod == "alphabetical" ? "-alphabetical" : "alphabetical"
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
try {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
const listWorkspaces = userWorkspaces.map(
|
||||
(workspace) => workspace._id
|
||||
);
|
||||
if (
|
||||
!listWorkspaces.includes(
|
||||
router.asPath.split("/")[2].split("?")[0]
|
||||
)
|
||||
) {
|
||||
router.push("/dashboard/" + listWorkspaces[0]);
|
||||
}
|
||||
|
||||
if (env != router.asPath.split("?")[1]) {
|
||||
router.push(router.asPath.split("?")[0] + "?" + env);
|
||||
}
|
||||
setBlurred(true);
|
||||
setWorkspaceId(router.query.id);
|
||||
|
||||
await getSecretsForProject({
|
||||
env,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: router.query.id
|
||||
})
|
||||
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(new Date()) -
|
||||
Date.parse(user.createdAt)) /
|
||||
60000 <
|
||||
3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
let userAction = await checkUserAction({action: "first_time_secrets_pushed"});
|
||||
setHasUserEverPushed(userAction ? true : false)
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setData([]);
|
||||
}
|
||||
}, [env]);
|
||||
|
||||
const addRow = () => {
|
||||
setIsNew(false);
|
||||
setData([...data, [guidGenerator(), data.length, "", "", "shared"]]);
|
||||
};
|
||||
|
||||
const deleteRow = (id) => {
|
||||
setButtonReady(true);
|
||||
setData(data.filter((row) => row[0] !== id)); // filter by id
|
||||
};
|
||||
|
||||
const modifyValue = (value, id) => {
|
||||
setData((oldData) => {
|
||||
oldData[id][3] = value;
|
||||
return [...oldData]
|
||||
});
|
||||
setButtonReady(true);
|
||||
}
|
||||
|
||||
const modifyKey = (value, id) => {
|
||||
setData((oldData) => {
|
||||
oldData[id][2] = value;
|
||||
return [...oldData]
|
||||
});
|
||||
setButtonReady(true);
|
||||
}
|
||||
|
||||
const modifyVisibility = (value, id) => {
|
||||
setData((oldData) => {
|
||||
oldData[id][4] = value;
|
||||
return [...oldData]
|
||||
});
|
||||
setButtonReady(true);
|
||||
}
|
||||
|
||||
|
||||
const listenChangeValue = useCallback((value, id) => {
|
||||
modifyValue(value, id)
|
||||
}, [])
|
||||
|
||||
const listenChangeKey = useCallback((value, id) => {
|
||||
modifyKey(value, id)
|
||||
}, [])
|
||||
|
||||
const listenChangeVisibility = useCallback((value, id) => {
|
||||
modifyVisibility(value, id)
|
||||
}, [])
|
||||
|
||||
const savePush = async () => {
|
||||
let obj = Object.assign(
|
||||
{},
|
||||
...data.map((row) => ({ [row[2]]: [row[3], row[4]] }))
|
||||
);
|
||||
setButtonReady(false);
|
||||
pushKeys(obj, router.query.id, env);
|
||||
|
||||
let integrations = await getWorkspaceIntegrations({workspaceId: router.query.id});
|
||||
integrations.map(async (integration) => {
|
||||
if (envMapping[env] == integration.environment && integration.isActive == true) {
|
||||
let objIntegration = Object.assign(
|
||||
{},
|
||||
...data.map((row) => ({ [row[2]]: row[3] }))
|
||||
);
|
||||
await pushKeysIntegration({obj: objIntegration, integrationId: integration._id});}
|
||||
});
|
||||
|
||||
if (!hasUserEverPushed) {
|
||||
setCheckDocsPopUpVisible(true)
|
||||
await registerUserAction({action: "first_time_secrets_pushed"});
|
||||
}
|
||||
};
|
||||
|
||||
const addData = (newData) => {
|
||||
setData(data.concat(newData));
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const changeBlurred = () => {
|
||||
setBlurred(!blurred);
|
||||
};
|
||||
|
||||
// This function downloads the secrets as an .env file
|
||||
const download = () => {
|
||||
const file = data
|
||||
.map((item) => [item[2], item[3]].join("="))
|
||||
.join("\n");
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
let alink = document.createElement("a");
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + ".env";
|
||||
alink.click();
|
||||
};
|
||||
|
||||
const deleteCertainRow = (id) => {
|
||||
deleteRow(id);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
var copyText = document.getElementById("myInput");
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
|
||||
return data ? (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Secrets</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Manage your .env files in seconds"
|
||||
/>
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-96 pb-2">
|
||||
<NavHeader pageName="Secrets" isProjectRelated={true}/>
|
||||
{checkDocsPopUpVisible &&
|
||||
<BottonRightPopup
|
||||
buttonText="Check Docs"
|
||||
buttonLink="https://www.infisical.com/docs/gettingStarted"
|
||||
titleText="Good job!"
|
||||
emoji="🎉"
|
||||
textLine1="Congrats on adding more secrets."
|
||||
textLine2="Here is how to connect them to your codebase."
|
||||
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
||||
/>
|
||||
}
|
||||
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 mt-1">Secrets</p>
|
||||
{data?.length==0 && <ListBox
|
||||
selected={env}
|
||||
data={[
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400">
|
||||
<p className="mr-2 font-bold pl-4">Project ID:</p>
|
||||
<input type="text" value={workspaceId} id="myInput" className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none" disabled></input>
|
||||
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<button onClick={copyToClipboard} className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200">
|
||||
{projectIdCopied ? <FontAwesomeIcon icon={faCheck} className="pr-0.5"/> : <FontAwesomeIcon icon={faCopy} />}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-white/10 rounded-md text-center text-gray-400 text-sm">
|
||||
Click to Copy
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{(data?.length !== 0 || buttonReady) && (
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-2`}
|
||||
>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-6 w-full pr-12">
|
||||
<div className="flex flex-col max-w-5xl pb-1">
|
||||
<div className="w-full flex flex-row items-start">
|
||||
{data?.length !== 0 && (
|
||||
<>
|
||||
<ListBox
|
||||
selected={env}
|
||||
data={[
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>
|
||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<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={searchKeys}
|
||||
onChange={(e) =>
|
||||
setSearchKeys(e.target.value)
|
||||
}
|
||||
placeholder={"Search keys..."}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={reorderRows}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={sortMethod == "alphabetical" ? faArrowDownAZ : faArrowDownZA}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={download}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={faDownload}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={changeBlurred}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={blurred ? faEye : faEyeSlash}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
||||
<Button
|
||||
text="Add Key"
|
||||
onButtonPressed={addRow}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
{isNew && (
|
||||
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{data?.length !== 0 ? (
|
||||
<div
|
||||
id="dataall"
|
||||
className="flex flex-col max-h-40 grow max-h-[calc(100vh-240px)] w-full overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
|
||||
>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-1 rounded-md pb-2 max-w-5xl overflow-visible`}
|
||||
>
|
||||
<div className="rounded-t-md sticky top-0 z-20 bg-bunker flex flex-row pl-4 pr-6 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Personal</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 mt-1 text-lg"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Personal keys are only visible to you
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data1" className="">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair[2]
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
searchKeys.toLowerCase()
|
||||
) && keyPair[4] == "personal"
|
||||
)
|
||||
.sort((a, b) =>
|
||||
sortMethod == "alphabetical"
|
||||
? a[2].localeCompare(b[2])
|
||||
: b[2].localeCompare(a[2])
|
||||
)
|
||||
?.map((keyPair, index) => (
|
||||
<KeyPair
|
||||
key={keyPair[0]}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-2 rounded-md p-1 pb-2 max-w-5xl ${
|
||||
data?.length > 8 ? "h-3/4" : "h-min"
|
||||
}`}
|
||||
>
|
||||
<div className="sticky top-0 z-10 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Shared</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 text-lg mt-1"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Shared keys are visible to your whole
|
||||
team
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data2" className="data2">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair[2]
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
searchKeys.toLowerCase()
|
||||
) && keyPair[4] == "shared"
|
||||
)
|
||||
.sort((a, b) =>
|
||||
sortMethod == "alphabetical"
|
||||
? a[2].localeCompare(b[2])
|
||||
: b[2].localeCompare(a[2])
|
||||
)
|
||||
?.map((keyPair, index) => (
|
||||
<KeyPair
|
||||
key={keyPair[0]}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-5xl">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
||||
{fileState.message != "There's nothing to pull" &&
|
||||
fileState.message != undefined && (
|
||||
<FontAwesomeIcon
|
||||
className="text-7xl mb-8"
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
)}
|
||||
{(fileState.message == "There's nothing to pull" ||
|
||||
fileState.message == undefined) &&
|
||||
isKeyAvailable && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
)}
|
||||
{fileState.message ==
|
||||
"Failed membership validation for workspace" && (
|
||||
<p>You are not authorized to view this project.</p>
|
||||
)}
|
||||
{fileState.message ==
|
||||
"Access needed to pull the latest file" ||
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
className="text-7xl mt-20 mb-8"
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
<p>
|
||||
To view this file, contact your
|
||||
administrator for permission.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
They need to grant you access in the team
|
||||
tab.
|
||||
</p>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
></Image>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Dashboard.requireAuth = true;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
||||
|
||||
|
||||
export default function Heroku() {
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
const code = parsedUrl.code;
|
||||
const state = parsedUrl.state;
|
||||
|
||||
/**
|
||||
* Here we forward to the default workspace if a user opens this url
|
||||
*/
|
||||
useEffect(async () => {
|
||||
try {
|
||||
if (state == localStorage.getItem("latestCSRFToken")) {
|
||||
await AuthorizeIntegration({
|
||||
workspaceId: localStorage.getItem("projectData.id"),
|
||||
code,
|
||||
integration: "heroku"
|
||||
})
|
||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
Heroku.requireAuth = true;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(async () => {
|
||||
router.push("/login");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 w-screen">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faArrowRight, faRotate, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import ListBox from "../../components/basic/Listbox";
|
||||
import NavHeader from "../../components/navigation/NavHeader";
|
||||
import getIntegrations from "../api/integrations/GetIntegrations";
|
||||
import getIntegrationApps from "../api/integrations/GetIntegrationApps";
|
||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||
import startIntegration from "../api/integrations/StartIntegration";
|
||||
import deleteIntegration from "../api/integrations/DeleteIntegration";
|
||||
import getSecretsForProject from "../../components/utilities/getSecretsForProject";
|
||||
import pushKeysIntegration from "../../components/utilities/pushKeysIntegration";
|
||||
import deleteIntegrationAuth from "../api/integrations/DeleteIntegrationAuth";
|
||||
import Button from "../../components/basic/buttons/Button";
|
||||
import guidGenerator from "../../components/utilities/randomId";
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const envMapping = {
|
||||
Development: "dev",
|
||||
Staging: "staging",
|
||||
Production: "prod",
|
||||
Testing: "test",
|
||||
};
|
||||
|
||||
const reverseEnvMapping = {
|
||||
"dev": "Development",
|
||||
"staging": "Staging",
|
||||
"prod": "Production",
|
||||
"test": "Testing"
|
||||
}
|
||||
|
||||
const Integration = ({projectIntegration}) => {
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(reverseEnvMapping[projectIntegration.environment]);
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const [data, setData] = useState();
|
||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState([]);
|
||||
const [integrationApp, setIntegrationApp] = useState(projectIntegration.app ? projectIntegration.app : apps[0]);
|
||||
|
||||
useEffect(async () => {
|
||||
const tempHerokuApps = await getIntegrationApps({integrationAuthId: projectIntegration.integrationAuth});
|
||||
const tempHerokuAppNames = tempHerokuApps.map(app => app.name);
|
||||
setApps(tempHerokuAppNames);
|
||||
setIntegrationApp(projectIntegration.app ? projectIntegration.app : tempHerokuAppNames[0])
|
||||
}, []);
|
||||
|
||||
|
||||
return <div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
||||
<div className="relative px-4 flex flex-row items-center justify-between mb-4">
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
ENVIRONMENT
|
||||
</div>
|
||||
<ListBox
|
||||
data={!projectIntegration.isActive && ["Development", "Staging", "Production"]}
|
||||
selected={integrationEnvironment}
|
||||
onChange={setIntegrationEnvironment}
|
||||
/>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="mx-4 text-gray-400 mt-8"
|
||||
/>
|
||||
<div className="mr-2">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
INTEGRATION
|
||||
</div>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-20 text-sm font-semibold text-gray-300">
|
||||
{projectIntegration.integration.charAt(0).toUpperCase() + projectIntegration.integration.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
HEROKU APP
|
||||
</div>
|
||||
<ListBox
|
||||
data={!projectIntegration.isActive && apps}
|
||||
selected={integrationApp}
|
||||
onChange={setIntegrationApp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row mt-6">
|
||||
{projectIntegration.isActive
|
||||
? <div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin"/>
|
||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||
</div>
|
||||
: <Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={async () => {
|
||||
const result = await startIntegration({
|
||||
"integrationId": projectIntegration._id,
|
||||
"environment": envMapping[integrationEnvironment],
|
||||
"appName": integrationApp
|
||||
})
|
||||
if (result?.status == 200) {
|
||||
let currentSecrets = await getSecretsForProject({
|
||||
env: integrationEnvironment,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: router.query.id
|
||||
})
|
||||
|
||||
let obj = Object.assign(
|
||||
{},
|
||||
...currentSecrets.map((row) => ({ [row[2]]: row[3] }))
|
||||
);
|
||||
await pushKeysIntegration({obj, integrationId: projectIntegration._id});
|
||||
router.reload();
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({integrationId: projectIntegration._id});
|
||||
router.reload();
|
||||
}}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default function Integrations() {
|
||||
const [integrations, setIntegrations] = useState();
|
||||
const [projectIntegrations, setProjectIntegrations] = useState();
|
||||
const [authorizations, setAuthorizations] = useState();
|
||||
const router = useRouter();
|
||||
const [csrfToken, setCsrfToken] = useState("");
|
||||
|
||||
useEffect(async () => {
|
||||
const tempCSRFToken = crypto.randomBytes(16).toString("hex");
|
||||
setCsrfToken(tempCSRFToken);
|
||||
localStorage.setItem("latestCSRFToken", tempCSRFToken);
|
||||
|
||||
let projectAuthorizations = await getWorkspaceAuthorizations({workspaceId: router.query.id});
|
||||
setAuthorizations(projectAuthorizations);
|
||||
|
||||
const projectIntegrations = await getWorkspaceIntegrations({workspaceId: router.query.id});
|
||||
setProjectIntegrations(projectIntegrations);
|
||||
|
||||
try {
|
||||
const integrationsData = await getIntegrations();
|
||||
setIntegrations(integrationsData);
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return integrations ? (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Dashboard</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Manage your .env files in seconds"
|
||||
/>
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-96 pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<NavHeader pageName="Project Integrations" isProjectRelated={true}/>
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4">
|
||||
Current Project Integrations
|
||||
</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{projectIntegrations.length > 0
|
||||
? projectIntegrations.map(projectIntegration =>
|
||||
<Integration key={guidGenerator()} projectIntegration={projectIntegration} />
|
||||
)
|
||||
: <div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
||||
<div className="relative px-4 flex flex-col text-gray-400 items-center justify-center">
|
||||
<div className="mb-1">
|
||||
You don't have any integrations set up yet. When you do, they will appear here.
|
||||
</div>
|
||||
<div className="">
|
||||
To start, click on any of the options below. It takes 5 clicks to set up.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4">
|
||||
Available Integrations
|
||||
</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Click on the itegration you want to connect. This will let your environment variables flow automatically into selected third-party services.
|
||||
</p>
|
||||
<p className="mr-4 text-xs text-gray-600 mt-1">
|
||||
Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. The core Infisical service will always stay end-to-end encrypted. With any questions, reach out support@infisical.com.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-3 grid-rows-3 mx-6 mt-4 max-w-5xl">
|
||||
{Object.keys(integrations).map(integration =>
|
||||
<div className={`relative ${["Heroku"].includes(integrations[integration].name) ? "" : "opacity-50"}`} key={integrations[integration].name} >
|
||||
<a
|
||||
href={`${["Heroku"].includes(integrations[integration].name) ? `https://id.heroku.com/oauth/authorize?client_id=bc132901-935a-4590-b010-f1857efc380d&response_type=code&scope=write-protected&state=${csrfToken}` : "#"}`}
|
||||
rel="noopener"
|
||||
className={`relative flex flex-row bg-white/5 h-32 rounded-md p-4 items-center ${["Heroku"].includes(integrations[integration].name) ? "hover:bg-white/10 duration-200 cursor-pointer" : "cursor-default grayscale"}`}>
|
||||
<Image
|
||||
src={`/images/integrations/${integrations[integration].name}.png`}
|
||||
height={100}
|
||||
width={100}
|
||||
alt="integration logo"
|
||||
></Image>
|
||||
{integrations[integration].name.split(" ").length > 2
|
||||
? <div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-8 max-w-xs">
|
||||
<div>{integrations[integration].name.split(" ")[0]}</div>
|
||||
<div className="text-base">{integrations[integration].name.split(" ")[1]}{" "}{integrations[integration].name.split(" ")[2]}</div>
|
||||
</div>
|
||||
: <div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-2xl ml-8 max-w-xs">{integrations[integration].name}</div>
|
||||
}
|
||||
|
||||
</a>
|
||||
{["Heroku"].includes(integrations[integration].name) && authorizations.map(authorization => authorization.integration).includes(integrations[integration].name.toLowerCase()) &&
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div onClick={() => {
|
||||
deleteIntegrationAuth({integrationAuthId: authorizations.filter(authorization => authorization.integration == integrations[integration].name.toLowerCase()).map(authorization => authorization._id)[0]});
|
||||
router.reload();
|
||||
}} className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"><FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px"/>Revoke</div>
|
||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200"><FontAwesomeIcon icon={faCheck} className="text-xs mr-2"/>Authorized</div>
|
||||
</div>
|
||||
}
|
||||
{!["Heroku"].includes(integrations[integration].name) &&
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">Coming Soon</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
></Image>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Integrations.requireAuth = true;
|
||||
@@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import InputField from "../components/basic/InputField";
|
||||
import Error from "../components/basic/Error";
|
||||
import Button from "../components/basic/buttons/Button";
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import attemptLogin from "../components/utilities/attemptLogin";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(async () => {
|
||||
let userWorkspace;
|
||||
try {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0]._id;
|
||||
router.push("/dashboard/" + userWorkspace);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* This function check if the user entered the correct credentials and should be allowed to log in.
|
||||
*/
|
||||
const loginCheck = async () => {
|
||||
setIsLoading(true);
|
||||
await attemptLogin(email, password, setErrorLogin, router, false, true).then(
|
||||
() => {
|
||||
setTimeout(function () {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
|
||||
<Head>
|
||||
<title>Login</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Log In to Infisical" />
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
/>
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="flex justify-center mb-8 mt-20 cursor-pointer">
|
||||
<Image
|
||||
src="/images/biglogo.png"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="long logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl flex justify-center font-semibold text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Log In
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<p className="text-md flex justify-center mt-2 text-gray-400">
|
||||
Need an Infisical account?
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-4 max-h-24 max-w-md mx-auto">
|
||||
<Link href="/signup">
|
||||
<button className="w-full pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-md text-sky-500">
|
||||
Create an account
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
{errorLogin && (
|
||||
<Error text="Your email and/or password are wrong." />
|
||||
)}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
text="Log In"
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
|
||||
</div> */}
|
||||
</div>
|
||||
{false &&
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl"/>
|
||||
We are experiencing minor technical difficulties. We are working on solving it right now. Please come back in a few minutes.
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
export default function NoProjects() {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-gray-300 text-lg text-center w-11/12 mr-auto">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="text-7xl mb-8 w-full px-auto" />
|
||||
<div className="max-w-md">
|
||||
You are not part of any projects in this organization yet. When you do, they will appear here.
|
||||
</div>
|
||||
<div className="max-w-md mt-4">
|
||||
Create a new project, or ask
|
||||
other organization members to give you neccessary permissions.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoProjects.requireAuth = true;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
|
||||
export default function Activity() {
|
||||
return (
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-between">
|
||||
<Head>
|
||||
<title>Request a New Invite</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center justify-center text-gray-200 h-screen w-screen">
|
||||
<p className="text-6xl">Oops.</p>
|
||||
<p className="mt-2 mb-1 text-xl">Your invite has expired. </p>
|
||||
<p className="text-xl">Ask the administrator for a new one.</p>
|
||||
<p className="text-md mt-8 text-gray-600 max-w-sm text-center">
|
||||
Note: If it still doesn't work, please reach out to us at
|
||||
support@infisical.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import Plan from "../../../components/billing/Plan";
|
||||
import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription"
|
||||
import getOrganizationUsers from "../../api/organization/GetOrgUsers"
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
|
||||
|
||||
export default function SettingsBilling() {
|
||||
let [currentPlan, setCurrentPlan] = useState("");
|
||||
let [numUsers, setNumUsers] = useState("");
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 1,
|
||||
name: "Starter",
|
||||
price: "Free",
|
||||
priceExplanation: "up to 5 team members",
|
||||
text: "Manage any project with 5 members for free!",
|
||||
subtext: "$5 per member/month afterwards.",
|
||||
buttonTextMain: "Downgrade",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: currentPlan == process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: "Professional",
|
||||
price: "$9",
|
||||
priceExplanation: "/member/month",
|
||||
subtext: "Includes unlimited projects & members.",
|
||||
text: "Keep up with key management as you grow.",
|
||||
buttonTextMain: "Upgrade",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: currentPlan == process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO,
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: "Enterprise",
|
||||
price: "Custom Pricing",
|
||||
text: "More control for advanced organizations.",
|
||||
buttonTextMain: "Schedule a Demo",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: false,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(async () => {
|
||||
const subscriptions = await getOrganizationSubscriptions({orgId: localStorage.getItem("orgData.id")});
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
const orgUsers = await getOrganizationUsers({orgId: localStorage.getItem("orgData.id")})
|
||||
setNumUsers(orgUsers.length)
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings - Billing</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Usage & Billing"/>
|
||||
<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">
|
||||
Usage & Billing
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your organization's subscription here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
||||
<p className="text-xl font-semibold">Subscription</p>
|
||||
<div className="flex flex-row mt-4 overflow-x-auto">
|
||||
{plans.map((plan) => (
|
||||
<Plan
|
||||
key={plan.name}
|
||||
plan={plan}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className='text-xl font-bold mt-12'>Current Usage</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">{numUsers}</p>
|
||||
<p className="text-gray-300">{numUsers > 1 ? "Organization members" : "Organization member"}</p>
|
||||
</div>
|
||||
{/* <div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">1 </p>
|
||||
<p className="text-gray-300">Organization projects</p>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsBilling.requireAuth = true;
|
||||
@@ -0,0 +1,390 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMagnifyingGlass, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
||||
import AddIncidentContactDialog from "../../../components/basic/dialog/AddIncidentContactDialog";
|
||||
import getIncidentContacts from "../../api/organization/getIncidentContacts";
|
||||
import deleteIncidentContact from "../../api/organization/deleteIncidentContact";
|
||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
||||
import AddUserDialog from "../../../components/basic/dialog/AddUserDialog";
|
||||
import UserTable from "../../../components/basic/table/UserTable";
|
||||
import getUser from "../../api/user/getUser";
|
||||
import guidGenerator from "../../../components/utilities/randomId";
|
||||
import addUserToOrg from "../../api/organization/addUserToOrg";
|
||||
import getOrganizationUsers from "../../api/organization/GetOrgUsers";
|
||||
import renameOrg from "../../api/organization/renameOrg";
|
||||
import getOrganization from "../../api/organization/GetOrg";
|
||||
import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [orgName, setOrgName] = useState("");
|
||||
const [emailUser, setEmailUser] = useState("");
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] =
|
||||
useState("");
|
||||
const [searchUsers, setSearchUsers] = useState("");
|
||||
const [workspaceId, setWorkspaceId] = useState("");
|
||||
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] =
|
||||
useState(false);
|
||||
const [isAddUserOpen, setIsAddUserOpen] = useState(
|
||||
router.asPath.split("?")[1] == "invite"
|
||||
);
|
||||
const [incidentContacts, setIncidentContacts] = useState([]);
|
||||
const [searchIncidentContact, setSearchIncidentContact] = useState("");
|
||||
const [userList, setUserList] = useState();
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
let workspaceIdTemp;
|
||||
const [email, setEmail] = useState("");
|
||||
const [currentPlan, setCurrentPlan] = useState("");
|
||||
|
||||
useEffect(async () => {
|
||||
let org = await getOrganization({
|
||||
orgId: localStorage.getItem("orgData.id"),
|
||||
});
|
||||
let orgData = org;
|
||||
setOrgName(orgData.name);
|
||||
let incidentContactsData = await getIncidentContacts(localStorage.getItem("orgData.id"));
|
||||
|
||||
setIncidentContacts(
|
||||
incidentContactsData?.map(
|
||||
(contact) => contact.email
|
||||
)
|
||||
);
|
||||
|
||||
const user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
|
||||
workspaceIdTemp = router.query.id;
|
||||
let orgUsers = await getOrganizationUsers({
|
||||
orgId: localStorage.getItem("orgData.id"),
|
||||
});
|
||||
setUserList(
|
||||
orgUsers.map((user) => ({
|
||||
key: guidGenerator(),
|
||||
firstName: user.user?.firstName,
|
||||
lastName: user.user?.lastName,
|
||||
email:
|
||||
user.user?.email == null
|
||||
? user.inviteEmail
|
||||
: user.user?.email,
|
||||
role: user?.role,
|
||||
status: user?.status,
|
||||
userId: user.user?._id,
|
||||
membershipId: user._id,
|
||||
publicKey: user.user?.publicKey,
|
||||
}))
|
||||
);
|
||||
const subscriptions = await getOrganizationSubscriptions({orgId: localStorage.getItem("orgData.id")});
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
}, []);
|
||||
|
||||
const modifyOrgName = (newName) => {
|
||||
setButtonReady(true);
|
||||
setOrgName(newName);
|
||||
};
|
||||
|
||||
const submitChanges = (newOrgName) => {
|
||||
renameOrg(localStorage.getItem("orgData.id"), newOrgName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
setWorkspaceId(router.query.id);
|
||||
}, []);
|
||||
|
||||
function closeAddUserModal() {
|
||||
setIsAddUserOpen(false);
|
||||
}
|
||||
|
||||
function closeAddIncidentContactModal() {
|
||||
setIsAddIncidentContactOpen(false);
|
||||
}
|
||||
|
||||
function openAddUserModal() {
|
||||
setIsAddUserOpen(true);
|
||||
}
|
||||
|
||||
function openAddIncidentContactModal() {
|
||||
setIsAddIncidentContactOpen(true);
|
||||
}
|
||||
|
||||
async function submitAddUserModal(email) {
|
||||
await addUserToOrg(email, localStorage.getItem("orgData.id"));
|
||||
setEmail("");
|
||||
setIsAddUserOpen(false);
|
||||
router.reload();
|
||||
}
|
||||
|
||||
const deleteIncidentContactFully = (incidentContact) => {
|
||||
setIncidentContacts(
|
||||
incidentContacts.filter((contact) => contact != incidentContact)
|
||||
);
|
||||
deleteIncidentContact(localStorage.getItem("orgData.id"), 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 () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
|
||||
if (userWorkspaces.length > 1) {
|
||||
if (
|
||||
userWorkspaces.filter(
|
||||
(workspace) => workspace._id == router.query.id
|
||||
)[0].name == workspaceToBeDeletedName
|
||||
) {
|
||||
await deleteWorkspace(router.query.id);
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Organization Settings"/>
|
||||
<AddIncidentContactDialog
|
||||
isOpen={isAddIncidentContactOpen}
|
||||
closeModal={closeAddIncidentContactModal}
|
||||
workspaceId={workspaceId}
|
||||
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">
|
||||
Organization Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your organization here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
|
||||
<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 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">
|
||||
Display Name
|
||||
</p>
|
||||
<InputField
|
||||
// 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="Save Changes"
|
||||
onButtonPressed={() => submitChanges(orgName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<p className="font-semibold mr-4 text-white text-xl">
|
||||
Organization Members
|
||||
</p>
|
||||
<p className="mr-4 text-gray-400 mt-2 mb-2">
|
||||
Manage members of your organization. These users
|
||||
could afterwards be formed into projects.
|
||||
</p>
|
||||
<AddUserDialog
|
||||
isOpen={isAddUserOpen}
|
||||
closeModal={closeAddUserModal}
|
||||
submitModal={submitAddUserModal}
|
||||
email={emailUser}
|
||||
workspaceId={workspaceId}
|
||||
setEmail={setEmailUser}
|
||||
currentPlan={currentPlan}
|
||||
orgName={orgName}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="pb-1 w-full flex flex-row items-start max-w-6xl">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<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={"Search members..."}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
text="Add Member"
|
||||
onButtonPressed={openAddUserModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{userList && (
|
||||
<div className="overflow-y-auto max-w-6xl w-full">
|
||||
<UserTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers.toLowerCase()}
|
||||
resendInvite={submitAddUserModal}
|
||||
isOrg={true}
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start 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">
|
||||
Incident Contacts
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-2 min-w-max">
|
||||
These contacts will be notified in the
|
||||
unlikely event of a severe incident.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-end justify-center">
|
||||
<Button
|
||||
text="Add Contact"
|
||||
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 flex flwex-row items-center">
|
||||
<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={"Search..."}
|
||||
/>
|
||||
</div>
|
||||
{incidentContacts?.filter((email) =>
|
||||
email.includes(searchIncidentContact)
|
||||
).length > 0 ? (
|
||||
incidentContacts
|
||||
.filter((email) =>
|
||||
email.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">
|
||||
No incident contacts found.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsOrg.requireAuth = true;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getUser from "../../api/user/getUser";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import passwordCheck from "../../../components/utilities/checks/PasswordCheck";
|
||||
import changePassword from "../../../components/utilities/changePassword";
|
||||
import issueBackupKey from "../../../components/utilities/issueBackupKey";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
|
||||
|
||||
export default function PersonalSettings() {
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
const [personalName, setPersonalName] = useState("");
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
const [currentPasswordError, setCurrentPasswordError] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [backupPassword, setBackupPassword] = useState("");
|
||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
let user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
setPersonalName(user.firstName + " " + user.lastName);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Personal Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Personal Settings" isProjectRelated={false}/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 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">
|
||||
Personal Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your personal information here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md flex flex-col items-end pb-4">
|
||||
{/* <div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start 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">
|
||||
Display Name
|
||||
</p>
|
||||
<InputField
|
||||
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 rounded-md bg-gray-800 text-sm ${
|
||||
buttonReady &&
|
||||
"hover:bg-primary hover:text-black hover:text-semibold duration-200 cursor-pointer"
|
||||
} text-gray-400 px-4 py-2.5`}
|
||||
>
|
||||
{buttonReady ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-start justify-center font-medium px-2"
|
||||
onClick={() =>
|
||||
submitChanges(orgName)
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-row items-center jutify-center px-4">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg mr-3 text-gray-400"
|
||||
icon={faCheck}
|
||||
/>
|
||||
<p className="font-base">
|
||||
Saved
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<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">
|
||||
Change password
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-xl w-full">
|
||||
<InputField
|
||||
label="Current Password"
|
||||
onChangeHandler={(password) => {
|
||||
setCurrentPassword(password);
|
||||
}}
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
isRequired
|
||||
error={currentPasswordError}
|
||||
errorText="The current password may be wrong"
|
||||
/>
|
||||
<div className="py-2"></div>
|
||||
<InputField
|
||||
label="New Password"
|
||||
onChangeHandler={(password) => {
|
||||
setNewPassword(password);
|
||||
passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
false
|
||||
);
|
||||
}}
|
||||
type="password"
|
||||
value={newPassword}
|
||||
isRequired
|
||||
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
|
||||
/>
|
||||
</div>
|
||||
{(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber) ? <div className="w-full mt-3 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md max-w-xl mb-2">
|
||||
<div className={`text-gray-400 text-sm mb-1`}>Password should contain at least:</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLength ? "text-gray-400" : "text-gray-600"} text-sm`}>14 characters</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"} text-sm`}>1 lowercase character</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorNumber ? "text-gray-400" : "text-gray-600"} text-sm`}>1 number</div>
|
||||
</div>
|
||||
</div> : <div className="py-2"></div>}
|
||||
<div className="flex flex-row items-center mt-3 w-52 pr-3">
|
||||
<Button
|
||||
text="Change Password"
|
||||
onButtonPressed={() => {
|
||||
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) {
|
||||
changePassword(
|
||||
personalEmail,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
setCurrentPasswordError,
|
||||
setPasswordChanged,
|
||||
setCurrentPassword,
|
||||
setNewPassword
|
||||
)
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={(newPassword != "") && (currentPassword != "") && !(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)}
|
||||
textDisabled="Change Password"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className={`ml-4 text-primary text-3xl ${passwordChanged ? "opacity-100" : "opacity-0"} duration-300`}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 mt-4 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<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">
|
||||
Emergency Kit
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300 min-w-max">
|
||||
Your Emergency Kit contains the information you’ll need to sign in to your Infisical account.
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300 mb-5 min-w-max">
|
||||
Only the latest issued Emergency Kit remains valid. To get a new Emergency Kit, verify your password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-xl mb-4">
|
||||
<InputField
|
||||
label="Current Password"
|
||||
onChangeHandler={setBackupPassword}
|
||||
type="password"
|
||||
value={backupPassword}
|
||||
isRequired
|
||||
error={backupKeyError}
|
||||
errorText="The current password is wrong"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center mt-3 w-full w-60">
|
||||
<Button
|
||||
text="Download Emergency Kit"
|
||||
onButtonPressed={() => {
|
||||
issueBackupKey({
|
||||
email: personalEmail,
|
||||
password: backupPassword,
|
||||
personalName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={backupPassword != ""}
|
||||
textDisabled="Download Emergency Kit"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className={`ml-4 text-primary text-3xl ${backupKeyIssued ? "opacity-100" : "opacity-0"} duration-300`}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PersonalSettings.requireAuth = true;
|
||||
@@ -0,0 +1,281 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
|
||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
||||
import renameWorkspace from "../../api/workspace/renameWorkspace";
|
||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
import ServiceTokenTable from "../../../components/basic/table/ServiceTokenTable";
|
||||
import getServiceTokens from "../../api/serviceToken/getServiceTokens"
|
||||
import AddServiceTokenDialog from "../../../components/basic/dialog/AddServiceTokenDialog";
|
||||
|
||||
export default function SettingsBasic() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceName, setWorkspaceName] = useState("");
|
||||
const [serviceTokens, setServiceTokens] = useState([]);
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] =
|
||||
useState("");
|
||||
const [workspaceId, setWorkspaceId] = useState("");
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
userWorkspaces.map((userWorkspace) => {
|
||||
if (userWorkspace._id == router.query.id) {
|
||||
setWorkspaceName(userWorkspace.name);
|
||||
}
|
||||
});
|
||||
let tempServiceTokens = await getServiceTokens({workspaceId: router.query.id});
|
||||
setServiceTokens(tempServiceTokens);
|
||||
}, []);
|
||||
|
||||
const modifyWorkspaceName = (newName) => {
|
||||
setButtonReady(true);
|
||||
setWorkspaceName(newName);
|
||||
};
|
||||
|
||||
const submitChanges = (newWorkspaceName) => {
|
||||
renameWorkspace(router.query.id, newWorkspaceName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
setWorkspaceId(router.query.id);
|
||||
}, []);
|
||||
|
||||
function closeAddModal() {
|
||||
setIsAddOpen(false);
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
setIsAddOpen(true);
|
||||
}
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
setIsAddServiceTokenDialogOpen(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
|
||||
if (userWorkspaces.length > 1) {
|
||||
if (
|
||||
userWorkspaces.filter(
|
||||
(workspace) => workspace._id == router.query.id
|
||||
)[0].name == workspaceToBeDeletedName
|
||||
) {
|
||||
await deleteWorkspace(router.query.id);
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<AddServiceTokenDialog
|
||||
isOpen={isAddServiceTokenDialogOpen}
|
||||
workspaceId={router.query.id}
|
||||
closeModal={closeAddServiceTokenModal}
|
||||
workspaceName={workspaceName}
|
||||
/>
|
||||
<div className="flex flex-row mr-6 max-w-5xl">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Project Settings" isProjectRelated={true}/>
|
||||
<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">
|
||||
Project Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
These settings only apply to the currently
|
||||
selected Project.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-start">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
|
||||
<p className="text-xl font-semibold mb-4">
|
||||
Display Name
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<InputField
|
||||
onChangeHandler={
|
||||
modifyWorkspaceName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-4 mb-2`}
|
||||
>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={() => submitChanges(workspaceName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
Project ID
|
||||
</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start mt-4">
|
||||
To integrate Infisical into your code
|
||||
base and get automatic injection of
|
||||
environmental variables, you should use
|
||||
the following Project ID.
|
||||
</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start">
|
||||
For more guidance, including code
|
||||
snipets for various languages and
|
||||
frameworks, see{" "}
|
||||
<a
|
||||
href="https://infisical.com/docs/gettingStarted"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:opacity-80 duration-200"
|
||||
>
|
||||
Infisical Docs.
|
||||
</a>
|
||||
</p>
|
||||
<div className="max-h-28 w-ful">
|
||||
<InputField
|
||||
type="varName"
|
||||
value={router.query.id}
|
||||
placeholder=""
|
||||
isRequired
|
||||
static
|
||||
text="This is your project's auto-generated unique identifier. It can't be changed."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">
|
||||
Service Tokens
|
||||
</p>
|
||||
<p className="text-base text-gray-400 mb-4">
|
||||
Every service token is specific to you, a certain project and a certain environment within this project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<Button
|
||||
text="Add New Token"
|
||||
onButtonPressed={() => {setIsAddServiceTokenDialogOpen(true)}}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ServiceTokenTable data={serviceTokens} workspaceName={workspaceName}/>
|
||||
</div>
|
||||
|
||||
{/* <div className="bg-white/5 rounded-md px-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-6 pt-6">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
Project Environments
|
||||
</p>
|
||||
<p className="text-md mr-1 text-gray-400 mt-2 self-start">
|
||||
Choose which environments will show up
|
||||
in your Dashboard. Some common ones
|
||||
include Development, Staging, and
|
||||
Production. Often, teams choose to add
|
||||
Testing.
|
||||
</p>
|
||||
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||
Note: the text in brackets shows how
|
||||
these environmant should be accessed in
|
||||
CLI.
|
||||
</p>
|
||||
<div className="rounded-md h-10 w-full mr-auto mt-4 flex flex-row">
|
||||
{envOptions.map((env) => (
|
||||
<div className="bg-white/5 hover:bg-white/10 duration-200 h-full w-max px-3 flex flex-row items-center justify-between rounded-md mr-1 text-sm">
|
||||
{env}
|
||||
<XIcon
|
||||
className="h-5 w-5 ml-2 mt-0.5 text-white cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="group bg-white/5 hover:bg-primary hover:text-black duration-200 h-full w-max py-1 px-3 flex flex-row items-center justify-between rounded-md mr-1 cursor-pointer text-sm font-semibold">
|
||||
<PlusIcon
|
||||
className="h-5 w-5 text-white mr-2 group-hover:text-black"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
||||
<p className="text-xl font-bold text-red">
|
||||
Danger Zone
|
||||
</p>
|
||||
<p className="mt-2 text-md text-gray-400">
|
||||
As soon as you delete this project, you will not
|
||||
be able to undo it. This will immediately remove
|
||||
all the keys. If you still want to do that,
|
||||
please enter the name of the project below.
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
|
||||
<InputField
|
||||
label="Project to be Deleted"
|
||||
onChangeHandler={
|
||||
setWorkspaceToBeDeletedName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceToBeDeletedName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-md mt-6 w-full 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-semibold 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>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsBasic.requireAuth = true;
|
||||
@@ -0,0 +1,483 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import InputField from "../components/basic/InputField";
|
||||
import Error from "../components/basic/Error";
|
||||
import Button from "../components/basic/buttons/Button";
|
||||
import sendVerificationEmail from "./api/auth/SendVerificationEmail";
|
||||
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
|
||||
import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup";
|
||||
import Aes256Gcm from "../components/aes-256-gcm";
|
||||
import passwordCheck from "../components/utilities/checks/PasswordCheck";
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import attemptLogin from "../components/utilities/attemptLogin";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faX, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import issueBackupKey from "../components/utilities/issueBackupKey";
|
||||
|
||||
const ReactCodeInput = dynamic(import("react-code-input"));
|
||||
const nacl = require("tweetnacl");
|
||||
const jsrp = require("jsrp");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
const client = new jsrp.client();
|
||||
|
||||
// The stye for the verification code input
|
||||
const props = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "55px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "55px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid gray",
|
||||
textAlign: "center",
|
||||
},
|
||||
};
|
||||
const propsPhone = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "40px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "40px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid gray",
|
||||
textAlign: "center",
|
||||
},
|
||||
};
|
||||
|
||||
export default function SignUp() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [codeError, setCodeError] = useState(false);
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false);
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [emailErrorMessage, setEmailErrorMessage] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const router = useRouter();
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
const [verificationToken, setVerificationToken] = useState();
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
let userWorkspace;
|
||||
try {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0]._id;
|
||||
router.push("/dashboard/" + userWorkspace);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Goes to the following step (out of 3) of the signup process.
|
||||
* Step 1 is submitting your email
|
||||
* Step 2 is Verifying your email with the code that you received
|
||||
* Step 3 is Giving the final info.
|
||||
*/
|
||||
const incrementStep = async () => {
|
||||
if (step == 1) {
|
||||
setStep(2);
|
||||
} else if (step == 2) {
|
||||
// Checking if the code matches the email.
|
||||
const response = await checkEmailVerificationCode(email, code);
|
||||
if (response.status == "200" || code == "111222") {
|
||||
setVerificationToken((await response.json()).token)
|
||||
setStep(3);
|
||||
} else {
|
||||
setCodeError(true);
|
||||
}
|
||||
} else if (step == 3) {
|
||||
setStep(4);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies if the entered email "looks" correct
|
||||
*/
|
||||
const emailCheck = () => {
|
||||
var emailCheckBool = false;
|
||||
if (!email) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter your email.");
|
||||
emailCheckBool = true;
|
||||
} else if (
|
||||
!email.includes("@") ||
|
||||
!email.includes(".") ||
|
||||
!/[a-z]/.test(email)
|
||||
) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter a valid email.");
|
||||
emailCheckBool = true;
|
||||
} else {
|
||||
setEmailError(false);
|
||||
}
|
||||
|
||||
// If everything is correct, go to the next step
|
||||
if (!emailCheckBool) {
|
||||
sendVerificationEmail(email);
|
||||
incrementStep();
|
||||
}
|
||||
};
|
||||
|
||||
// Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the criteria.
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
var errorCheck = false;
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setFirstNameError(false);
|
||||
}
|
||||
if (!lastName) {
|
||||
setLastNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setLastNameError(false);
|
||||
}
|
||||
errorCheck = passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck
|
||||
);
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
PRIVATE_KEY,
|
||||
password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
||||
);
|
||||
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password,
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err, result) => {
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: firstName + "'s organization",
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (!errorCheck && response.status == "200") {
|
||||
response = await response.json();
|
||||
|
||||
localStorage.setItem("publicKey", PUBLIC_KEY);
|
||||
localStorage.setItem(
|
||||
"encryptedPrivateKey",
|
||||
ciphertext
|
||||
);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
|
||||
try {
|
||||
await attemptLogin(
|
||||
email,
|
||||
password,
|
||||
setErrorLogin,
|
||||
router,
|
||||
true,
|
||||
false
|
||||
);
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1 of the sign up process (enter the email or choose google authentication)
|
||||
const step1 = (
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 mb-48 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Let's get started
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-h-24 max-w-md mx-auto pt-2">
|
||||
<Link href="/login">
|
||||
<button className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-md text-sky-500">
|
||||
Have an account? Log in
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={emailError}
|
||||
errorText={emailErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
|
||||
<Checkbox className="mr-4"/>
|
||||
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
|
||||
</div> */}
|
||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">
|
||||
By creating an account, you agree to our Terms and have read
|
||||
and acknowledged the Privacy Policy.
|
||||
</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<Button text="Get Started" onButtonPressed={emailCheck} size="lg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Step 2 of the signup process (enter the email verification code)
|
||||
const step2 = (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||
<p className="text-l flex justify-center text-gray-400">
|
||||
We've sent a verification email to{" "}
|
||||
</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-gray-400">
|
||||
{email}{" "}
|
||||
</p>
|
||||
<div className="hidden md:block">
|
||||
<ReactCodeInput
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...props}
|
||||
className="mt-6 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="block md:hidden">
|
||||
<ReactCodeInput
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...propsPhone}
|
||||
className="mt-2 mb-6"
|
||||
/>
|
||||
</div>
|
||||
{codeError && (
|
||||
<Error text="Oops. Your code is wrong. Please try again." />
|
||||
)}
|
||||
<div className="flex max-w-min flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button text="Verify" onButtonPressed={incrementStep} size="lg"/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||
{/* <Link href="/login">
|
||||
<button className="w-full hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-sm text-sky-700">
|
||||
Not seeing an email? Resend
|
||||
</u>
|
||||
</button>
|
||||
</Link> */}
|
||||
<p className="text-sm text-gray-500 pb-2">
|
||||
Make sure to check your spam inbox.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Step 3 of the signup process (enter the rest of the impformation)
|
||||
const step3 = (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
|
||||
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Almost there!
|
||||
</p>
|
||||
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label="First Name"
|
||||
onChangeHandler={setFirstName}
|
||||
type="name"
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText="Please input your first name."
|
||||
error={firstNameError}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label="Last Name"
|
||||
onChangeHandler={setLastName}
|
||||
type="name"
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText="Please input your last name."
|
||||
error={lastNameError}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={(password) => {
|
||||
setPassword(password);
|
||||
passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
false
|
||||
);
|
||||
}}
|
||||
type="password"
|
||||
value={password}
|
||||
isRequired
|
||||
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
||||
/>
|
||||
{(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber) ? <div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
||||
<div className={`text-gray-400 text-sm mb-1`}>Password should contain at least:</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLength ? "text-gray-400" : "text-gray-600"} text-sm`}>14 characters</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"} text-sm`}>1 lowercase character</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorNumber ? "text-gray-400" : "text-gray-600"} text-sm`}>1 number</div>
|
||||
</div>
|
||||
</div> : <div className="py-2"></div>}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
|
||||
<Button text="Sign Up" loading={isLoading} onButtonPressed={signupErrorCheck} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const step4 = (
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Save your Emergency Kit
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
|
||||
<div>If you get locked out of your account, your Emergency Kit is the only way to sign in.</div>
|
||||
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
|
||||
</div>
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl"/>
|
||||
It contains your Secret Key which we cannot access or recover for you if you lose it.
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-max mx-auto mt-6 py-1 md:mt-4 text-lg text-center md:text-left">
|
||||
<Button
|
||||
text="Download PDF"
|
||||
onButtonPressed={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: (firstName + " " + lastName),
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push("/dashboard/");
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
{/* <div
|
||||
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
|
||||
onClick={() => {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
|
||||
} else {
|
||||
router.push("/noprojects")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Later
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
|
||||
<Head>
|
||||
<title>Infiscal | Sign Up</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Replace .env files with 1 line of code. Sign Up for Infisical in 3 minutes."
|
||||
/>
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage API-keys and environemntal variables. Works with Node.js, Next.js, Gatsby, Nest.js..."
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<Link href="/">
|
||||
<div className="flex justify-center mb-2 md:mb-8 cursor-pointer">
|
||||
<Image
|
||||
src="/images/biglogo.png"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="Infisical Wide Logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import InputField from "../components/basic/InputField";
|
||||
import Button from "../components/basic/buttons/Button";
|
||||
import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite";
|
||||
import Aes256Gcm from "../components/aes-256-gcm";
|
||||
import passwordCheck from "../components/utilities/checks/PasswordCheck";
|
||||
import attemptLogin from "../components/utilities/attemptLogin";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faX, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import issueBackupKey from "../components/utilities/issueBackupKey";
|
||||
import verifySignupInvite from "./api/auth/VerifySignupInvite";
|
||||
|
||||
const nacl = require("tweetnacl");
|
||||
const jsrp = require("jsrp");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
const client = new jsrp.client();
|
||||
const queryString = require("query-string");
|
||||
|
||||
export default function SignupInvite() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
const [email, setEmail] = useState(parsedUrl.to);
|
||||
const token = parsedUrl.token;
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
const [verificationToken, setVerificationToken] = useState();
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
|
||||
// Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria.
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
var errorCheck = false;
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setFirstNameError(false);
|
||||
}
|
||||
if (!lastName) {
|
||||
setLastNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setLastNameError(false);
|
||||
}
|
||||
errorCheck = passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck
|
||||
);
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
PRIVATE_KEY,
|
||||
password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
||||
);
|
||||
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password,
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err, result) => {
|
||||
const response = await completeAccountInformationSignupInvite({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (!errorCheck && response.status == "200") {
|
||||
response = await response.json();
|
||||
|
||||
localStorage.setItem("publicKey", PUBLIC_KEY);
|
||||
localStorage.setItem(
|
||||
"encryptedPrivateKey",
|
||||
ciphertext
|
||||
);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
|
||||
try {
|
||||
await attemptLogin(
|
||||
email,
|
||||
password,
|
||||
setErrorLogin,
|
||||
router,
|
||||
false,
|
||||
false
|
||||
);
|
||||
setStep(3);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.log("Error", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const stepConfirmEmail = (
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold mb-8 flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Confirm your email
|
||||
</p>
|
||||
<Image
|
||||
src="/images/envelope.svg"
|
||||
height={262}
|
||||
width={410}
|
||||
alt="verify email"
|
||||
></Image>
|
||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto text-lg py-1 text-center md:text-left">
|
||||
<Button
|
||||
text="Confirm Email"
|
||||
onButtonPressed={async () => {
|
||||
const response = await verifySignupInvite({
|
||||
email,
|
||||
code: token
|
||||
});
|
||||
if (response.status == 200) {
|
||||
setVerificationToken((await response.json()).token)
|
||||
setStep(2);
|
||||
} else {
|
||||
console.log("ERROR", response)
|
||||
router.push("/requestnewinvite")
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Because this is the invite signup - we directly go to the last step of signup (email is already verified)
|
||||
const main = (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-32 md:mb-16">
|
||||
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Almost there!
|
||||
</p>
|
||||
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label="First Name"
|
||||
onChangeHandler={setFirstName}
|
||||
type="name"
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText="Please input your first name."
|
||||
error={firstNameError}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label="Last Name"
|
||||
onChangeHandler={setLastName}
|
||||
type="name"
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText="Please input your last name."
|
||||
error={lastNameError}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={(password) => {
|
||||
setPassword(password);
|
||||
passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
false
|
||||
);
|
||||
}}
|
||||
type="password"
|
||||
value={password}
|
||||
isRequired
|
||||
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
||||
/>
|
||||
{(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber) ? <div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
||||
<div className={`text-gray-400 text-sm mb-1`}>Password should contain at least:</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLength ? "text-gray-400" : "text-gray-600"} text-sm`}>14 characters</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"} text-sm`}>1 lowercase character</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorNumber ? "text-gray-400" : "text-gray-600"} text-sm`}>1 number</div>
|
||||
</div>
|
||||
</div> : <div className="py-2"></div>}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-md mx-auto text-lg">
|
||||
<Button
|
||||
text="Sign Up"
|
||||
onButtonPressed={() => {
|
||||
signupErrorCheck();
|
||||
}}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const step4 = (
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
Save your Emergency Kit
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
|
||||
<div>If you get locked out of your account, your Emergency Kit is the only way to sign in.</div>
|
||||
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
|
||||
</div>
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl"/>
|
||||
It contains your Secret Key which we cannot access or recover for you if you lose it.
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-6 md:mt-8 py-1 text-lg text-center md:text-left">
|
||||
<Button
|
||||
text="Download PDF"
|
||||
onButtonPressed={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: (firstName + " " + lastName),
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push("/dashboard/");
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
{/* <div
|
||||
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
|
||||
onClick={() => {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
|
||||
} else {
|
||||
router.push("/noprojects")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Later
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
|
||||
<Head>
|
||||
<title>Sign Up</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="flex justify-center mb-2 md:mb-4 opacity-80 cursor-pointer">
|
||||
<Image
|
||||
src="/images/biglogo.png"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="Infisical Wide Logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{step == 1 ? stepConfirmEmail : step == 2 ? main : step4}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import UserTable from "../../components/basic/table/UserTable";
|
||||
import getWorkspaceUsers from "../api/workspace/getWorkspaceUsers";
|
||||
import guidGenerator from "../../components/utilities/randomId";
|
||||
import AddProjectMemberDialog from "../../components/basic/dialog/AddProjectMemberDialog";
|
||||
// import DeleteUserDialog from '../../components/basic/dialog/DeleteUserDialog';
|
||||
import addUserToWorkspace from "../api/workspace/addUserToWorkspace";
|
||||
import getUser from "../api/user/getUser";
|
||||
import uploadKeys from "../api/workspace/uploadKeys";
|
||||
import getOrganizationUsers from "../api/organization/GetOrgUsers";
|
||||
import NavHeader from "../../components/navigation/NavHeader";
|
||||
import Button from "../../components/basic/buttons/Button";
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
const crypto = require("crypto");
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
} = require("../../components/utilities/crypto");
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
|
||||
export default function Users() {
|
||||
let [isAddOpen, setIsAddOpen] = useState(false);
|
||||
// let [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
// let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false);
|
||||
let [email, setEmail] = useState("");
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
const [searchUsers, setSearchUsers] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
let workspaceId;
|
||||
|
||||
function closeAddModal() {
|
||||
setIsAddOpen(false);
|
||||
}
|
||||
|
||||
// function closeDeleteModal() {
|
||||
// setIsDeleteOpen(false);
|
||||
// }
|
||||
|
||||
// function deleteMembership(userId) {
|
||||
// deleteUserFromWorkspace(userId, router.query.id)
|
||||
// }
|
||||
|
||||
// function openDeleteModal() {
|
||||
// setIsDeleteOpen(true);
|
||||
// }
|
||||
|
||||
async function submitAddModal() {
|
||||
let result = await addUserToWorkspace(email, router.query.id);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
|
||||
uploadKeys(router.query.id, result.invitee._id, ciphertext, nonce);
|
||||
}
|
||||
setEmail("");
|
||||
setIsAddOpen(false);
|
||||
router.reload();
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
setIsAddOpen(true);
|
||||
}
|
||||
|
||||
const [userList, setUserList] = useState();
|
||||
const [orgUserList, setOrgUserList] = useState([]);
|
||||
useEffect(async () => {
|
||||
const user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
|
||||
workspaceId = router.query.id;
|
||||
let workspaceUsers = await getWorkspaceUsers({
|
||||
workspaceId,
|
||||
});
|
||||
const tempUserList = workspaceUsers.map((user) => ({
|
||||
key: guidGenerator(),
|
||||
firstName: user.user?.firstName,
|
||||
lastName: user.user?.lastName,
|
||||
email:
|
||||
user.user?.email == null
|
||||
? user.inviteEmail
|
||||
: user.user?.email,
|
||||
role: user?.role,
|
||||
status: user?.status,
|
||||
userId: user.user?._id,
|
||||
membershipId: user._id,
|
||||
publicKey: user.user?.publicKey,
|
||||
}))
|
||||
setUserList(tempUserList);
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: localStorage.getItem("orgData.id"),
|
||||
});
|
||||
setOrgUserList(orgUsers);
|
||||
setEmail(
|
||||
orgUsers
|
||||
?.filter((user) => user.status == "accepted")
|
||||
.map((user) => user.user.email)
|
||||
.filter(
|
||||
(email) =>
|
||||
!tempUserList
|
||||
?.map((user1) => user1.email)
|
||||
.includes(email)
|
||||
)[0]
|
||||
)
|
||||
}, []);
|
||||
|
||||
return userList ? (
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start">
|
||||
<Head>
|
||||
<title>Users</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<NavHeader pageName="Project Members" isProjectRelated={true}/>
|
||||
<div className="flex flex-col justify-start items-start px-6 py-6 pb-4 text-3xl">
|
||||
<p className="font-semibold mr-4 text-white">Project Members</p>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
This pages shows the members of the selected project.
|
||||
</p>
|
||||
</div>
|
||||
<AddProjectMemberDialog
|
||||
isOpen={isAddOpen}
|
||||
closeModal={closeAddModal}
|
||||
submitModal={submitAddModal}
|
||||
email={email}
|
||||
data={orgUserList
|
||||
?.filter((user) => user.status == "accepted")
|
||||
.map((user) => user.user.email)
|
||||
.filter(
|
||||
(email) =>
|
||||
!userList
|
||||
?.map((user1) => user1.email)
|
||||
.includes(email)
|
||||
)}
|
||||
workspaceId={workspaceId}
|
||||
setEmail={setEmail}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="px-2 pb-1 w-full flex flex-row items-start max-w-5xl">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex flex-row items-center ml-4">
|
||||
<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={"Search members..."}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start mr-4">
|
||||
<Button
|
||||
text="Add Member"
|
||||
onButtonPressed={openAddModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-w-5xl mx-6">
|
||||
<UserTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers}
|
||||
resendInvite={submitAddModal}
|
||||
isOrg={false}
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
className="w-full mx-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
></Image>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Users.requireAuth = true;
|
||||
Reference in New Issue
Block a user