Merge pull request #701 from Infisical/main

Update branch
This commit is contained in:
vmatsiiako
2023-06-30 16:54:26 -07:00
committed by GitHub
176 changed files with 11767 additions and 7719 deletions
+10 -6
View File
@@ -158,7 +158,9 @@ export default function Activity() {
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<NavHeader pageName="Audit Logs" isProjectRelated />
<div className="ml-6">
<NavHeader pageName="Audit Logs" isProjectRelated />
</div>
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
)}
@@ -184,11 +186,13 @@ export default function Activity() {
/>
</div>
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
text="You can see more logs if you switch to Infisical's Business/Professional Plan."
/>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
/>
)}
</div>
);
}
@@ -1,33 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
name: string;
expiresIn: number;
}
/**
* This route adds an API key for the user
* @param {object} obj
* @param {string} obj.name - name of the API key
* @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns
*/
const addAPIKey = ({ name, expiresIn }: Props) =>
SecurityClient.fetchCall("/api/v2/api-key/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name,
expiresIn
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to add API key");
return undefined;
});
export default addAPIKey;
@@ -1,27 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
apiKeyId: string;
}
/**
* This route revokes the API key with id [apiKeyId]
* @param {object} obj
* @param {string} obj.apiKeyId - id of the API key to delete
* @returns
*/
const deleteAPIKey = ({ apiKeyId }: Props) =>
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to delete API key");
return undefined;
});
export default deleteAPIKey;
@@ -1,22 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route gets API keys for the user
* @param {*} param0
* @returns
*/
const getAPIKeys = () =>
SecurityClient.fetchCall("/api/v2/api-key", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).apiKeyData;
}
console.log("Failed to get API keys");
return undefined;
});
export default getAPIKeys;
+27 -444
View File
@@ -1,409 +1,18 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
import IntegrationSection from "@app/components/integrations/IntegrationSection";
import NavHeader from "@app/components/navigation/NavHeader";
import { IntegrationsPage } from "@app/views/IntegrationsPage";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
import getBot from "../api/bot/getBot";
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
import deleteIntegration from "../api/integrations/DeleteIntegration";
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
import getAWorkspace from "../api/workspace/getAWorkspace";
import getLatestFileKey from "../api/workspace/getLatestFileKey";
interface IntegrationAuth {
_id: string;
integration: string;
workspace: string;
createdAt: string;
updatedAt: string;
}
interface Integration {
_id: string;
isActive: boolean;
app: string | null;
appId: string | null;
createdAt: string;
updatedAt: string;
environment: string;
integration: string;
targetEnvironment: string;
workspace: string;
secretPath:string;
integrationAuth: string;
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
const [environments, setEnvironments] = useState<
{
name: string;
slug: string;
}[]
>([]);
const [integrations, setIntegrations] = useState<Integration[]>([]);
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] =
useState<IntegrationOption | null>(null);
const router = useRouter();
const workspaceId = router.query.id as string;
type Props = {
frameworkIntegrations: typeof frameworkIntegrationOptions;
};
const Integration = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
useEffect(() => {
(async () => {
try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options
setCloudIntegrationOptions(await getIntegrationOptions());
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
workspaceId
})
);
// get project integrations
setIntegrations(
await getWorkspaceIntegrations({
workspaceId
})
);
// get project bot
setBot(await getBot({ workspaceId }));
} catch (err) {
console.error(err);
}
})();
}, []);
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const handleBotActivate = async () => {
let botKey;
try {
if (bot) {
// case: there is a bot
const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: true,
botKey
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = "";
switch (integrationAuth.integration) {
case "azure-key-vault":
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case "heroku":
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case "vercel":
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case "netlify":
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case "github":
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
/**
* Open dialog to activate bot if bot is not active.
* Otherwise, start integration [integrationOption]
* @param {Object} integrationOption - an integration option
* @param {String} integrationOption.name
* @param {String} integrationOption.type
* @param {String} integrationOption.docsLink
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
try {
const integrationAuthX = integrationAuths.find(
(integrationAuth) => integrationAuth.integration === integrationOption.slug
);
if (!bot.isActive) {
await handleBotActivate();
}
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
* @param {Object} obj
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
*/
const handleDeleteIntegrationAuth = async ({
integrationAuth: deletedIntegrationAuth
}: {
integrationAuth: IntegrationAuth;
}) => {
try {
const newIntegrations = integrations.filter(
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
);
setIntegrationAuths(
integrationAuths.filter(
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
)
);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration [integration]
* @param {Object} obj
* @param {Integration} obj.integration - integration to delete
*/
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
try {
const deletedIntegration = await deleteIntegration({
integrationId: integration._id
});
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
return (
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
<>
<Head>
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -411,52 +20,26 @@ export default function Integrations() {
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Head>
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
<NavHeader pageName={t("integrations.title")} isProjectRelated />
<ActivateBotDialog
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
bot={bot}
setBot={setBot}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
{cloudIntegrationOptions.length > 0 && bot ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
integrationOptionPress={(integrationOption: IntegrationOption) => {
if (!bot.isActive) {
// case: bot is not active -> open modal to activate bot
setIsActivateBotDialogOpen(true);
return;
}
integrationOptionPress(integrationOption);
}}
integrationAuths={integrationAuths}
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/>
) : (
<>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div>
</div>
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
</>
);
}
};
Integrations.requireAuth = true;
export const getStaticProps = () => {
return {
props: {
frameworkIntegrations: frameworkIntegrationOptions
}
};
};
export const getStaticPaths = async () => {
return {
paths: [], // indicates that no page needs be created at build time
fallback: "blocking" // indicates the type of fallback
};
};
Integration.requireAuth = true;
export default Integration;
@@ -98,7 +98,7 @@ export default function ChecklyCreateIntegrationPage() {
>
Checkly Integration
</CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
<FormControl label="Infisical Project Environment" className="mt-4 px-6">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<FormControl label="Secrets Path" className="mt-4 px-6">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
+4 -83
View File
@@ -1,99 +1,20 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Plan from "@app/components/billing/Plan";
import NavHeader from "@app/components/navigation/NavHeader";
import { useSubscription } from "@app/context";
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
export default function SettingsBilling() {
const { subscription } = useSubscription();
const { t } = useTranslation();
const plans = [
{
key: 1,
name: t("billing.starter.name")!,
price: t("billing.free")!,
priceExplanation: t("billing.starter.price-explanation")!,
text: t("billing.starter.text")!,
subtext: t("billing.starter.subtext")!,
buttonTextMain: t("billing.downgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "starter"
},
{
key: 2,
name: "Team",
price: "$8",
priceExplanation: t("billing.professional.price-explanation")!,
text: "Unlimited members, up to 10 projects. Additional developer experience features.",
buttonTextMain: t("billing.upgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "team" || subscription?.slug === "team-annual"
},
{
key: 3,
name: t("billing.professional.name")!,
price: "$18",
priceExplanation: t("billing.professional.price-explanation")!,
text: t("billing.enterprise.text")!,
subtext: t("billing.professional.subtext")!,
buttonTextMain: t("billing.upgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "pro" || subscription?.slug === "pro-annual"
},
{
key: 4,
name: t("billing.enterprise.name")!,
price: t("billing.custom-pricing")!,
text: "Boost the security and efficiency of your engineering teams.",
buttonTextMain: t("billing.schedule-demo")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "enterprise"
}
];
return (
<div className="flex flex-col justify-between bg-bunker-800 pb-4 text-white">
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-row">
<div className="w-full pb-2">
<NavHeader pageName={t("billing.title")} />
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t("billing.title")}</p>
<p className="mr-4 text-base font-normal text-gray-400">{t("billing.description")}</p>
</div>
</div>
<div className="ml-6 flex w-max flex-col text-mineshaft-50">
<p className="text-xl font-semibold">{t("billing.subscription")}</p>
<div className="mt-4 grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 overflow-x-auto">
{plans.map((plan) => (
<Plan key={plan.name} plan={plan} />
))}
</div>
{/* <p className="mt-12 text-xl font-bold">{t("billing.current-usage")}</p>
<div className="flex flex-row">
<div className="mr-4 mt-8 flex w-60 flex-col items-center justify-center rounded-md bg-white/5 pt-6 pb-10 text-gray-300">
<p className="text-6xl font-bold">{numUsers}</p>
<p className="text-gray-300">
Organization members
</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>
<BillingSettingsPage />
</div>
);
}
SettingsBilling.requireAuth = true;
SettingsBilling.requireAuth = true;
+4 -308
View File
@@ -1,322 +1,18 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import { faBan,faCheck, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import ListBox from "@app/components/basic/Listbox";
import ApiKeyTable from "@app/components/basic/table/ApiKeyTable";
import NavHeader from "@app/components/navigation/NavHeader";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import changePassword from "@app/components/utilities/cryptography/changePassword";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import {
useGetCommonPasswords,
useRevokeAllSessions} from "@app/hooks/api";
import { SecuritySection } from "@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection";
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
import getAPIKeys from "../../api/apiKey/getAPIKeys";
import getUser from "../../api/user/getUser";
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
import { PersonalSettingsPage } from "@app/views/Settings/PersonalSettingsPage";
export default function PersonalSettings() {
const { data: commonPasswords } = useGetCommonPasswords();
const [personalEmail, setPersonalEmail] = useState("");
const [personalName, setPersonalName] = useState("");
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);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions();
const { t, i18n } = useTranslation();
const router = useRouter();
const lang = router.locale ?? "en";
const setLanguage = async (to: string) => {
router.push(router.asPath, router.asPath, { locale: to });
localStorage.setItem("lang", to);
i18n.changeLanguage(to);
};
useEffect(() => {
const load = async () => {
try {
const user = await getUser();
setApiKeys(await getAPIKeys());
setPersonalEmail(user.email);
setPersonalName(`${user.firstName} ${user.lastName}`);
} catch (err) {
console.error(err);
}
};
load();
}, []);
const closeAddApiKeyModal = () => {
setIsAddApiKeyDialogOpen(false);
};
const { t } = useTranslation();
return (
<div className="flex max-h-screen flex-col justify-between bg-bunker-800 text-white">
<div className="bg-bunker-800 text-white h-full">
<Head>
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddApiKeyDialog
isOpen={isAddApiKeyDialogOpen}
closeModal={closeAddApiKeyModal}
apiKeys={apiKeys}
setApiKeys={setApiKeys}
/>
<div className="flex flex-row">
<div className="max-h-screen w-full pb-2">
<NavHeader pageName={t("settings.personal.title")} isProjectRelated={false} />
<div className="ml-6 mt-8 mb-6 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t("settings.personal.title")}</p>
<p className="mr-4 text-base font-normal text-gray-400">
{t("settings.personal.description")}
</p>
</div>
</div>
<div className="ml-6 mr-6 flex max-w-5xl flex-col text-mineshaft-50">
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<p className="self-start text-xl font-semibold">
{t("settings.personal.change-language")}
</p>
<div className="w-ful mt-4 max-h-28">
<ListBox
isSelected={lang}
onChange={setLanguage}
data={["en", "ko", "fr", "es"]}
text={`${t("common.language")}: `}
/>
</div>
</div>
<SecuritySection />
<div className="mt-2 mb-8 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4">
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">
{t("settings.personal.api-keys.title")}
</p>
<p className="text-sm text-gray-400">
{t("settings.personal.api-keys.description")}
</p>
</div>
<div className="mt-2 w-40">
<Button
text={String(t("settings.personal.api-keys.add-new"))}
onButtonPressed={() => {
setIsAddApiKeyDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ApiKeyTable data={apiKeys} setApiKeys={setApiKeys as any} />
</div>
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
<div className="flex w-full max-w-3xl flex-col justify-between">
<p className="mb-3 min-w-max text-xl font-semibold">
{t("section.password.change")}
</p>
</div>
</div>
<div className="w-full max-w-xl">
<InputField
label={t("section.password.current") as string}
onChangeHandler={(password) => {
setCurrentPassword(password);
}}
type="password"
value={currentPassword}
isRequired
error={currentPasswordError}
errorText={t("section.password.current-wrong") as string}
autoComplete="current-password"
id="current-password"
/>
<div className="py-2" />
<InputField
label={t("section.password.new") as string}
onChangeHandler={(password) => {
setNewPassword(password);
checkPassword({
password,
commonPasswords,
setErrors
});
}}
type="password"
value={newPassword}
isRequired
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
</div>
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
)}
<div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button
text={t("section.password.change") as string}
onButtonPressed={() => {
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (!errorCheck) {
changePassword(
personalEmail,
currentPassword,
newPassword,
setCurrentPasswordError,
setPasswordChanged,
setCurrentPassword,
setNewPassword
);
}
}}
active={Object.keys(errors).length === 0 && currentPassword !== ""}
color="mineshaft"
size="md"
textDisabled={t("section.password.change") as string}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-3xl text-primary ${
passwordChanged ? "opacity-100" : "opacity-0"
} duration-300`}
/>
</div>
</div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="my-4 flex w-full flex-row justify-between">
<p className="text-xl font-semibold w-full">
Sessions
</p>
<div className="w-40">
<Button
text="Revoke all"
onButtonPressed={async () => {
await revokeAllSessions.mutateAsync();
router.push("/login");
}}
color="mineshaft"
icon={faBan}
size="md"
/>
</div>
</div>
<p className="mb-5 text-sm text-mineshaft-300">
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
</p>
</div>
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
<div className="flex w-full max-w-3xl flex-col justify-between">
<p className="mb-3 min-w-max text-xl font-semibold">
{t("settings.personal.emergency.name")}
</p>
<p className="min-w-max text-sm text-mineshaft-300">
{t("settings.personal.emergency.text1")}
</p>
<p className="mb-5 min-w-max text-sm text-mineshaft-300">
{t("settings.personal.emergency.text2")}
</p>
</div>
</div>
<div className="mb-4 w-full max-w-xl">
<InputField
label={t("section.password.current") as string}
onChangeHandler={setBackupPassword}
type="password"
value={backupPassword}
isRequired
error={backupKeyError}
errorText={t("section.password.current-wrong") as string}
autoComplete="current-password"
id="current-password"
/>
</div>
<div className="mt-3 flex w-60 flex-row items-center">
<Button
text={t("settings.personal.emergency.download") as string}
onButtonPressed={() => {
issueBackupKey({
email: personalEmail,
password: backupPassword,
personalName,
setBackupKeyError,
setBackupKeyIssued
});
}}
color="mineshaft"
size="md"
active={backupPassword !== ""}
textDisabled={t("settings.personal.emergency.download") as string}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-3xl text-primary ${
backupKeyIssued ? "opacity-100" : "opacity-0"
} duration-300`}
/>
</div>
</div>
</div>
</div>
</div>
<PersonalSettingsPage />
</div>
);
}
+3 -1
View File
@@ -154,7 +154,9 @@ export default function Users() {
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
<div className="ml-6">
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
</div>
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
</div>