mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 18:27:19 +00:00
Finish preliminary making user settings, org settings styling similar to usage and billing page
This commit is contained in:
@@ -1,72 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import { APIKeyData } from "../../models";
|
|
||||||
import { getSaltRounds } from "../../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return API key data for user with id [req.user_id]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const apiKeyData = await APIKeyData.find({
|
|
||||||
user: req.user._id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new API key data for user with id [req.user._id]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const { name, expiresIn } = req.body;
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString("hex");
|
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
let apiKeyData = await new APIKeyData({
|
|
||||||
name,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
expiresAt,
|
|
||||||
user: req.user._id,
|
|
||||||
secretHash,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return api key data without sensitive data
|
|
||||||
// FIX: fix this any
|
|
||||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
|
||||||
|
|
||||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
|
||||||
|
|
||||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKey,
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete API key data with id [apiKeyDataId].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const { apiKeyDataId } = req.params;
|
|
||||||
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import * as usersController from "./usersController";
|
|||||||
import * as organizationsController from "./organizationsController";
|
import * as organizationsController from "./organizationsController";
|
||||||
import * as workspaceController from "./workspaceController";
|
import * as workspaceController from "./workspaceController";
|
||||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||||
import * as apiKeyDataController from "./apiKeyDataController";
|
|
||||||
import * as secretController from "./secretController";
|
import * as secretController from "./secretController";
|
||||||
import * as secretsController from "./secretsController";
|
import * as secretsController from "./secretsController";
|
||||||
import * as serviceAccountsController from "./serviceAccountsController";
|
import * as serviceAccountsController from "./serviceAccountsController";
|
||||||
@@ -18,7 +17,6 @@ export {
|
|||||||
organizationsController,
|
organizationsController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
serviceTokenDataController,
|
serviceTokenDataController,
|
||||||
apiKeyDataController,
|
|
||||||
secretController,
|
secretController,
|
||||||
secretsController,
|
secretsController,
|
||||||
serviceAccountsController,
|
serviceAccountsController,
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
import {
|
import {
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
User,
|
User,
|
||||||
|
APIKeyData,
|
||||||
|
TokenVersion
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
|
import { getSaltRounds } from "../../config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current user.
|
* Return the current user.
|
||||||
@@ -117,3 +123,106 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
|||||||
organizations,
|
organizations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return API keys belonging to current user.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||||
|
const apiKeyData = await APIKeyData.find({
|
||||||
|
user: req.user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(apiKeyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new API key for current user.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createAPIKey = async (req: Request, res: Response) => {
|
||||||
|
const { name, expiresIn } = req.body;
|
||||||
|
|
||||||
|
const secret = crypto.randomBytes(16).toString("hex");
|
||||||
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
let apiKeyData = await new APIKeyData({
|
||||||
|
name,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
expiresAt,
|
||||||
|
user: req.user._id,
|
||||||
|
secretHash,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// return api key data without sensitive data
|
||||||
|
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
||||||
|
|
||||||
|
if (!apiKeyData) throw new Error("Failed to find API key data");
|
||||||
|
|
||||||
|
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apiKey,
|
||||||
|
apiKeyData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete API key with id [apiKeyDataId] belonging to current user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteAPIKey = async (req: Request, res: Response) => {
|
||||||
|
const { apiKeyDataId } = req.params;
|
||||||
|
|
||||||
|
const apiKeyData = await APIKeyData.findOneAndDelete({
|
||||||
|
_id: new Types.ObjectId(apiKeyDataId),
|
||||||
|
user: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apiKeyData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return active sessions (TokenVersion) belonging to user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMySessions = async (req: Request, res: Response) => {
|
||||||
|
const tokenVersions = await TokenVersion.find({
|
||||||
|
user: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(tokenVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke all active sessions belong to user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteMySessions = async (req: Request, res: Response) => {
|
||||||
|
await TokenVersion.updateMany({
|
||||||
|
user: req.user._id,
|
||||||
|
}, {
|
||||||
|
$inc: {
|
||||||
|
refreshVersion: 1,
|
||||||
|
accessVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: "Successfully revoked all sessions"
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
secrets as v2SecretsRouter,
|
secrets as v2SecretsRouter,
|
||||||
serviceTokenData as v2ServiceTokenDataRouter,
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
serviceAccounts as v2ServiceAccountsRouter,
|
serviceAccounts as v2ServiceAccountsRouter,
|
||||||
apiKeyData as v2APIKeyDataRouter,
|
|
||||||
environment as v2EnvironmentRouter,
|
environment as v2EnvironmentRouter,
|
||||||
tags as v2TagsRouter,
|
tags as v2TagsRouter,
|
||||||
} from "./routes/v2";
|
} from "./routes/v2";
|
||||||
@@ -138,7 +137,6 @@ const main = async () => {
|
|||||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||||
app.use("/api/v2/api-key", v2APIKeyDataRouter);
|
|
||||||
|
|
||||||
// v3 routes (experimental)
|
// v3 routes (experimental)
|
||||||
app.use("/api/v3/auth", v3AuthRouter);
|
app.use("/api/v3/auth", v3AuthRouter);
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
const router = express.Router();
|
|
||||||
import { body, param } from "express-validator";
|
|
||||||
import {
|
|
||||||
requireAuth,
|
|
||||||
validateRequest,
|
|
||||||
} from "../../middleware";
|
|
||||||
import { apiKeyDataController } from "../../controllers/v2";
|
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
} from "../../variables";
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
apiKeyDataController.getAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
body("name").exists().trim(),
|
|
||||||
body("expiresIn"), // measured in ms
|
|
||||||
validateRequest,
|
|
||||||
apiKeyDataController.createAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
"/:apiKeyDataId",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
param("apiKeyDataId").exists().trim(),
|
|
||||||
validateRequest,
|
|
||||||
apiKeyDataController.deleteAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -7,7 +7,6 @@ import secret from "./secret"; // deprecated
|
|||||||
import secrets from "./secrets";
|
import secrets from "./secrets";
|
||||||
import serviceTokenData from "./serviceTokenData";
|
import serviceTokenData from "./serviceTokenData";
|
||||||
import serviceAccounts from "./serviceAccounts";
|
import serviceAccounts from "./serviceAccounts";
|
||||||
import apiKeyData from "./apiKeyData";
|
|
||||||
import environment from "./environment"
|
import environment from "./environment"
|
||||||
import tags from "./tags"
|
import tags from "./tags"
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ export {
|
|||||||
secrets,
|
secrets,
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
serviceAccounts,
|
serviceAccounts,
|
||||||
apiKeyData,
|
|
||||||
environment,
|
environment,
|
||||||
tags,
|
tags,
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
} from "../../middleware";
|
} from "../../middleware";
|
||||||
import { body } from "express-validator";
|
import { body, param } from "express-validator";
|
||||||
import { usersController } from "../../controllers/v2";
|
import { usersController } from "../../controllers/v2";
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_API_KEY,
|
AUTH_MODE_API_KEY,
|
||||||
@@ -37,4 +37,49 @@ router.get(
|
|||||||
usersController.getMyOrganizations
|
usersController.getMyOrganizations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/me/api-keys",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.getMyAPIKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/me/api-keys",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
body("name").exists().isString().trim(),
|
||||||
|
body("expiresIn").isNumeric(),
|
||||||
|
validateRequest,
|
||||||
|
usersController.createAPIKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/me/api-keys/:apiKeyDataId",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
param("apiKeyDataId").exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
usersController.deleteAPIKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get( // new
|
||||||
|
"/me/sessions",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.getMySessions
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete( // new
|
||||||
|
"/me/sessions",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.deleteMySessions
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { Fragment, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
|
|
||||||
import addAPIKey from "@app/pages/api/apiKey/addAPIKey";
|
|
||||||
|
|
||||||
import Button from "../buttons/Button";
|
|
||||||
import InputField from "../InputField";
|
|
||||||
import ListBox from "../Listbox";
|
|
||||||
|
|
||||||
const expiryMapping = {
|
|
||||||
"1 day": 86400,
|
|
||||||
"7 days": 604800,
|
|
||||||
"1 month": 2592000,
|
|
||||||
"6 months": 15552000,
|
|
||||||
"12 months": 31104000
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
closeModal: () => void;
|
|
||||||
// TODO: These never and any will be filled by single folder that contains types and hooks about the API
|
|
||||||
apiKeys: any[];
|
|
||||||
setApiKeys: (arg: any[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: convert to TS
|
|
||||||
const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => {
|
|
||||||
const [apiKey, setApiKey] = useState("");
|
|
||||||
const [apiKeyName, setApiKeyName] = useState("");
|
|
||||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
|
|
||||||
const [apiKeyCopied, setApiKeyCopied] = useState(false);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const generateAPIKey = async () => {
|
|
||||||
const newApiKey = await addAPIKey({
|
|
||||||
name: apiKeyName,
|
|
||||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping]
|
|
||||||
});
|
|
||||||
|
|
||||||
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
|
|
||||||
setApiKey(newApiKey.apiKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
function copyToClipboard() {
|
|
||||||
// Get the text field
|
|
||||||
const copyText = document.getElementById("apiKey") as HTMLInputElement;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
setApiKeyCopied(true);
|
|
||||||
setTimeout(() => setApiKeyCopied(false), 2000);
|
|
||||||
// Alert the copied text
|
|
||||||
// alert("Copied the text: " + copyText.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeAddApiKeyModal = () => {
|
|
||||||
closeModal();
|
|
||||||
setApiKeyName("");
|
|
||||||
setApiKey("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="z-50">
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
{apiKey === "" ? (
|
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
{t("section.api-key.add-dialog.title")}
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2 mb-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{t("section.api-key.add-dialog.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2 max-h-28">
|
|
||||||
<InputField
|
|
||||||
label={t("section.api-key.add-dialog.name")}
|
|
||||||
onChangeHandler={setApiKeyName}
|
|
||||||
type="varName"
|
|
||||||
value={apiKeyName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-28">
|
|
||||||
<ListBox
|
|
||||||
isSelected={apiKeyExpiresIn}
|
|
||||||
onChange={setApiKeyExpiresIn}
|
|
||||||
data={["1 day", "7 days", "1 month", "6 months", "12 months"]}
|
|
||||||
isFull
|
|
||||||
text={`${t("common.expired-in")}: `}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-max">
|
|
||||||
<div className="mt-6 flex w-max flex-col justify-start">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => generateAPIKey()}
|
|
||||||
color="mineshaft"
|
|
||||||
text={t("section.api-key.add-dialog.add") as string}
|
|
||||||
textDisabled={t("section.api-key.add-dialog.add") as string}
|
|
||||||
size="md"
|
|
||||||
active={apiKeyName !== ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
) : (
|
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
{t("section.api-key.add-dialog.copy-service-token")}
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2 mb-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{t("section.api-key.add-dialog.copy-service-token-description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiKey}
|
|
||||||
disabled
|
|
||||||
id="apiKey"
|
|
||||||
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
|
|
||||||
/>
|
|
||||||
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
|
|
||||||
{apiKey}
|
|
||||||
</div>
|
|
||||||
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
|
|
||||||
>
|
|
||||||
{apiKeyCopied ? (
|
|
||||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
|
|
||||||
{t("common.click-to-copy")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flex w-max flex-col justify-start">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => closeAddApiKeyModal()}
|
|
||||||
color="mineshaft"
|
|
||||||
text="Close"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
)}
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddApiKeyDialog;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { faX } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
|
|
||||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
|
||||||
import guidGenerator from "../../utilities/randomId";
|
|
||||||
import Button from "../buttons/Button";
|
|
||||||
|
|
||||||
interface TokenProps {
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
expiresAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServiceTokensProps {
|
|
||||||
data: TokenProps[];
|
|
||||||
setApiKeys: (value: TokenProps[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the component that we utilize for the api key table
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {any[]} obj.data - current state of the api key table
|
|
||||||
* @param {function} obj.setApiKeys - updating the state of the api key table
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
return (
|
|
||||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
|
||||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
|
||||||
<table className="w-full my-1">
|
|
||||||
<thead className="text-bunker-300 text-sm font-light">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
|
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
|
|
||||||
<th aria-label="button" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data?.length > 0 ? (
|
|
||||||
data?.map((row) => (
|
|
||||||
<tr
|
|
||||||
key={guidGenerator()}
|
|
||||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
|
||||||
>
|
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
|
||||||
{row.name}
|
|
||||||
</td>
|
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
|
||||||
</td>
|
|
||||||
<td className="py-2 border-mineshaft-700 border-t">
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => {
|
|
||||||
deleteAPIKey({ apiKeyId: row._id });
|
|
||||||
setApiKeys(data.filter((token) => token._id !== row._id));
|
|
||||||
createNotification({
|
|
||||||
text: `'${row.name}' API key has been revoked.`,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
size="icon-sm"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
|
||||||
No API keys yet
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiKeyTable;
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import StripeRedirect from "@app/pages/api/organization/StripeRedirect";
|
|
||||||
|
|
||||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
plan: {
|
|
||||||
name: string;
|
|
||||||
price: string;
|
|
||||||
priceExplanation?: string;
|
|
||||||
text: string;
|
|
||||||
subtext?: string;
|
|
||||||
buttonTextMain: string;
|
|
||||||
buttonTextSecondary: string;
|
|
||||||
current: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Plan({ plan }: Props) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative flex flex-col justify-between border min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
|
||||||
plan.name !== "Starter" && plan.current === true ? "border-2 border-primary" : "border-mineshaft-600"
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-row justify-between items-center relative z-10">
|
|
||||||
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flwx-row items-end justify-start mb-4">
|
|
||||||
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
|
|
||||||
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
|
|
||||||
</div>
|
|
||||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
|
|
||||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
{plan.current === false ? (
|
|
||||||
<>
|
|
||||||
{plan.buttonTextMain === "Schedule a Demo" ? (
|
|
||||||
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
|
||||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
|
||||||
{plan.buttonTextMain}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 text-gray-400 font-semibold ${
|
|
||||||
plan.buttonTextMain === "Downgrade"
|
|
||||||
? "hover:bg-red hover:text-white hover:border-red"
|
|
||||||
: "hover:bg-primary hover:text-black hover:border-primary"
|
|
||||||
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
StripeRedirect({
|
|
||||||
orgId: tempLocalStorage("orgData.id")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{plan.buttonTextMain}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
|
|
||||||
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
|
|
||||||
{plan.buttonTextSecondary}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
|
||||||
plan.name !== "Starter" && plan.current === true ? "bg-primary" : "bg-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,7 @@ export default function NavHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-4 flex flex-row items-center pt-6">
|
<div className="flex flex-row items-center pt-6">
|
||||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/* eslint-disable new-cap */
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
|
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
|
||||||
|
import SRP1 from "@app/pages/api/auth/SRP1";
|
||||||
|
|
||||||
|
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
||||||
|
import { deriveArgonKey } from "./cryptography/crypto";
|
||||||
|
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||||
|
|
||||||
|
const clientOldPassword = new jsrp.client();
|
||||||
|
const clientNewPassword = new jsrp.client();
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
email: string;
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
||||||
|
let serverPublicKey; let salt;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||||
|
|
||||||
|
const res = await SRP1({ clientPublicKey });
|
||||||
|
|
||||||
|
serverPublicKey = res.serverPublicKey;
|
||||||
|
salt = res.salt;
|
||||||
|
|
||||||
|
clientOldPassword.setSalt(salt);
|
||||||
|
clientOldPassword.setServerPublicKey(serverPublicKey);
|
||||||
|
|
||||||
|
const clientProof = clientOldPassword.getProof();
|
||||||
|
|
||||||
|
clientNewPassword.init({ username: email, password: newPassword }, async () => {
|
||||||
|
clientNewPassword.createVerifier(async (err, result) => {
|
||||||
|
try {
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password: newPassword,
|
||||||
|
salt: result.salt,
|
||||||
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: localStorage.getItem("PRIVATE_KEY") as string,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString("hex"),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
await changePassword2({
|
||||||
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (err2) {
|
||||||
|
console.error(err2);
|
||||||
|
reject(err2);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default attemptChangePassword;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useGetCommonPasswords,
|
useGetCommonPasswords,
|
||||||
useRevokeAllSessions,
|
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useVerifyMfaToken} from "./queries"
|
useVerifyMfaToken} from "./queries"
|
||||||
|
|||||||
@@ -51,15 +51,6 @@ export const useGetAuthToken = () =>
|
|||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useRevokeAllSessions = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const { data } = await apiRequest.delete("/api/v1/auth/sessions");
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchCommonPasswords = async () => {
|
const fetchCommonPasswords = async () => {
|
||||||
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
|
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
|
||||||
return data || [];
|
return data || [];
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ export {
|
|||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
useAddUserToWs,
|
useAddUserToWs,
|
||||||
|
useCreateAPIKey,
|
||||||
|
useDeleteAPIKey,
|
||||||
useDeleteOrgMembership,
|
useDeleteOrgMembership,
|
||||||
|
useGetMyAPIKeys,
|
||||||
|
useGetMySessions,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useUpdateOrgUserRole
|
useRevokeMySessions,
|
||||||
} from "./queries";
|
useUpdateOrgUserRole} from "./queries";
|
||||||
|
|||||||
@@ -12,16 +12,20 @@ import {
|
|||||||
AddUserToOrgDTO,
|
AddUserToOrgDTO,
|
||||||
AddUserToWsDTO,
|
AddUserToWsDTO,
|
||||||
AddUserToWsRes,
|
AddUserToWsRes,
|
||||||
|
APIKeyData,
|
||||||
|
CreateAPIKeyRes,
|
||||||
DeletOrgMembershipDTO,
|
DeletOrgMembershipDTO,
|
||||||
OrgUser,
|
OrgUser,
|
||||||
|
TokenVersion,
|
||||||
UpdateOrgUserRoleDTO,
|
UpdateOrgUserRoleDTO,
|
||||||
User
|
User} from "./types";
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ["user"] as const,
|
getUser: ["user"] as const,
|
||||||
userAction: ["user-action"] as const,
|
userAction: ["user-action"] as const,
|
||||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"]
|
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||||
|
myAPIKeys: ["api-keys"] as const,
|
||||||
|
mySessions: ["sessions"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUserDetails = async () => {
|
export const fetchUserDetails = async () => {
|
||||||
@@ -167,3 +171,90 @@ export const useLogoutUser = () =>
|
|||||||
localStorage.setItem("PRIVATE_KEY", "");
|
localStorage.setItem("PRIVATE_KEY", "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetMyAPIKeys = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.myAPIKeys,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<APIKeyData[]>(
|
||||||
|
"/api/v2/users/me/api-keys"
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateAPIKey = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
name,
|
||||||
|
expiresIn
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post<CreateAPIKeyRes>(
|
||||||
|
"/api/v2/users/me/api-keys",
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
expiresIn
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteAPIKey = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (apiKeyDataId: string) => {
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
`/api/v2/users/me/api-keys/${apiKeyDataId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetMySessions = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.mySessions,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TokenVersion[]>(
|
||||||
|
"/api/v2/users/me/sessions"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRevokeMySessions = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
console.log("useRevokeAllSessions 1");
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
"/api/v2/users/me/sessions"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("useRevokeAllSessions 2: ", data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.mySessions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -61,3 +61,27 @@ export type AddUserToOrgDTO = {
|
|||||||
inviteeEmail: string;
|
inviteeEmail: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateAPIKeyRes = {
|
||||||
|
apiKey: string;
|
||||||
|
apiKeyData: APIKeyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APIKeyData = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
user: string;
|
||||||
|
lastUsed: string;
|
||||||
|
createdAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenVersion = {
|
||||||
|
_id: string;
|
||||||
|
user: string;
|
||||||
|
userAgent: string;
|
||||||
|
ip: string;
|
||||||
|
lastUsed: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
@@ -158,7 +158,9 @@ export default function Activity() {
|
|||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
<meta property="og:image" content="/images/message.png" />
|
<meta property="og:image" content="/images/message.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<NavHeader pageName="Audit Logs" isProjectRelated />
|
<div className="ml-6">
|
||||||
|
<NavHeader pageName="Audit Logs" isProjectRelated />
|
||||||
|
</div>
|
||||||
{currentSidebarAction && (
|
{currentSidebarAction && (
|
||||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -412,7 +412,9 @@ export default function Integrations() {
|
|||||||
<meta name="og:description" content={t("integrations.description") as string} />
|
<meta name="og:description" content={t("integrations.description") as string} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
|
<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 />
|
<div className="ml-6">
|
||||||
|
<NavHeader pageName={t("integrations.title")} isProjectRelated />
|
||||||
|
</div>
|
||||||
<ActivateBotDialog
|
<ActivateBotDialog
|
||||||
isOpen={isActivateBotDialogOpen}
|
isOpen={isActivateBotDialogOpen}
|
||||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||||
|
|||||||
@@ -1,322 +1,18 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
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 { PersonalSettingsPage } from "@app/views/Settings/PersonalSettingsPage";
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PersonalSettings() {
|
export default function PersonalSettings() {
|
||||||
const { data: commonPasswords } = useGetCommonPasswords();
|
const { t } = useTranslation();
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<AddApiKeyDialog
|
<PersonalSettingsPage />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,9 @@ export default function Users() {
|
|||||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</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">
|
<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>
|
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const DashboardEnvOverview = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<div className="relative right-5">
|
<div className="relative right-5 ml-4">
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
<FormProvider {...method}>
|
<FormProvider {...method}>
|
||||||
<form autoComplete="off" className="h-full">
|
<form autoComplete="off" className="h-full">
|
||||||
{/* breadcrumb row */}
|
{/* breadcrumb row */}
|
||||||
<div className="relative right-6 -top-2 mb-2">
|
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||||
<NavHeader
|
<NavHeader
|
||||||
pageName={t("dashboard.title")}
|
pageName={t("dashboard.title")}
|
||||||
currentEnv={
|
currentEnv={
|
||||||
|
|||||||
@@ -9,16 +9,13 @@ import {
|
|||||||
export const BillingSettingsPage = () => {
|
export const BillingSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="h-full py-8 px-4">
|
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||||
<NavHeader pageName={t("billing.title")} />
|
<div className="max-w-screen-lg w-full">
|
||||||
|
<NavHeader pageName={t("billing.title")} />
|
||||||
<div className="ml-4 flex text-3xl mt-8 items-start max-w-screen-lg">
|
<div className="my-8">
|
||||||
<div className="flex-1">
|
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||||
<p className="font-semibold text-gray-200">{t("billing.title")}</p>
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<BillingTabGroup />
|
<BillingTabGroup />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ export const CurrentPlanSection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
|
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
+2
-2
@@ -39,7 +39,7 @@ export const PreviewSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
|
{!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mt-8 flex items-center bg-mineshaft-600 max-w-screen-lg">
|
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600 max-w-screen-lg">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
||||||
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
||||||
@@ -53,7 +53,7 @@ export const PreviewSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && data && (
|
{!isLoading && data && (
|
||||||
<div className="flex mt-8 max-w-screen-lg">
|
<div className="flex mb-6 max-w-screen-lg">
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
||||||
<p className="mb-2 text-gray-400">Current plan</p>
|
<p className="mb-2 text-gray-400">Current plan</p>
|
||||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">Starter</p>
|
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">Starter</p>
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ export const CompanyNameSection = () => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||||
Business name
|
Business name
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ export const InvoiceEmailSection = () => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">
|
<h2 className="text-xl font-semibold flex-1 text-white mb-8">
|
||||||
Invoice email recipient
|
Invoice email recipient
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ export const PmtMethodsSection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Payment Methods
|
Payment Methods
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
// import Button from "@app/components/basic/buttons/Button";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { TaxIDModal } from "./TaxIDModal";
|
import { TaxIDModal } from "./TaxIDModal";
|
||||||
@@ -15,7 +14,7 @@ export const TaxIDSection = () => {
|
|||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Tax ID
|
Tax ID
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import { InvoicesTable } from "./InvoicesTable";
|
|||||||
|
|
||||||
export const BillingReceiptsTab = () => {
|
export const BillingReceiptsTab = () => {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
|
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
|
||||||
<InvoicesTable />
|
<InvoicesTable />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ const tabs = [
|
|||||||
export const BillingTabGroup = () => {
|
export const BillingTabGroup = () => {
|
||||||
return (
|
return (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="mt-8 border-b-2 border-mineshaft-800 max-w-screen-lg">
|
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800 max-w-screen-lg">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab as={Fragment} key={tab.key}>
|
<Tab as={Fragment} key={tab.key}>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
|
|||||||
@@ -1,282 +1,66 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
|
||||||
import {
|
|
||||||
useAddIncidentContact,
|
|
||||||
useAddUserToOrg,
|
|
||||||
useDeleteIncidentContact,
|
|
||||||
useDeleteOrgMembership,
|
|
||||||
useGetOrgIncidentContact,
|
|
||||||
useGetOrgUsers,
|
|
||||||
useGetUserWorkspaceMemberships,
|
|
||||||
useGetUserWsKey,
|
|
||||||
useUpdateOrgUserRole,
|
|
||||||
useUploadWsKey
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgIncidentContactsTable,
|
OrgIncidentContactsSection,
|
||||||
OrgMembersTable,
|
OrgMembersSection,
|
||||||
OrgNameChangeSection,
|
OrgNameChangeSection,
|
||||||
OrgServiceAccountsTable
|
OrgServiceAccountsTable
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
export const OrgSettingsPage = () => {
|
export const OrgSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { user } = useUser();
|
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
|
|
||||||
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
|
|
||||||
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
|
|
||||||
useGetUserWorkspaceMemberships(orgId);
|
|
||||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || "");
|
|
||||||
const { data: incidentContact, isLoading: IsIncidentContactLoading } =
|
|
||||||
useGetOrgIncidentContact(orgId);
|
|
||||||
|
|
||||||
|
|
||||||
const removeUserOrgMembership = useDeleteOrgMembership();
|
|
||||||
const addUserToOrg = useAddUserToOrg();
|
|
||||||
const updateOrgUserRole = useUpdateOrgUserRole();
|
|
||||||
const uploadWsKey = useUploadWsKey();
|
|
||||||
const addIncidentContact = useAddIncidentContact();
|
|
||||||
const removeIncidentContact = useDeleteIncidentContact();
|
|
||||||
|
|
||||||
const [completeInviteLink, setcompleteInviteLink] = useState<string | undefined>("");
|
|
||||||
|
|
||||||
const isMoreUsersNotAllowed = subscription?.memberLimit ? (subscription.membersUsed >= subscription.memberLimit) : false;
|
|
||||||
|
|
||||||
const onRemoveUserOrgMembership = async (membershipId: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully removed user from org",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove user from the organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onAddUserToOrg = async (email: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await addUserToOrg.mutateAsync({
|
|
||||||
organizationId: currentOrg?._id,
|
|
||||||
inviteeEmail: email
|
|
||||||
});
|
|
||||||
setcompleteInviteLink(data?.completeInviteLink);
|
|
||||||
|
|
||||||
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
|
|
||||||
if (!data.completeInviteLink) {
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully invited user to the organization.",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to invite user to org",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdateOrgUserRole = async (membershipId: string, role: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated user role",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update user role",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGrantUserAccess = async (userId: string, publicKey: string) => {
|
|
||||||
try {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
|
||||||
if (!PRIVATE_KEY || !wsKey) return;
|
|
||||||
|
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
|
||||||
const key = decryptAssymmetric({
|
|
||||||
ciphertext: wsKey.encryptedKey,
|
|
||||||
nonce: wsKey.nonce,
|
|
||||||
publicKey: wsKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadWsKey.mutateAsync({
|
|
||||||
userId,
|
|
||||||
nonce,
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
workspaceId: currentWorkspace?._id || ""
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to grant access to user",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddIncidentContact = async (email: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await addIncidentContact.mutateAsync({ orgId, email });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully added incident contact",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to add incident contact",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveIncidentContact = async (email: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeIncidentContact.mutateAsync({ orgId, email });
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully removed incident contact",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove incident contact",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||||
|
<div className="max-w-screen-lg w-full">
|
||||||
<NavHeader pageName={t("settings.org.title")} />
|
<NavHeader pageName={t("settings.org.title")} />
|
||||||
<div className="my-8 ml-8 max-w-5xl">
|
<div className="my-8">
|
||||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
||||||
<p className="text-base font-normal text-gray-400">{t("settings.org.description")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
|
||||||
<OrgNameChangeSection />
|
<OrgNameChangeSection />
|
||||||
<div className="mb-6 w-full rounded-md bg-white/5 p-6">
|
<OrgMembersSection />
|
||||||
<p className="mr-4 mb-4 text-xl font-semibold text-white">
|
<OrgIncidentContactsSection />
|
||||||
{t("section.members.org-members")}
|
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
</p>
|
|
||||||
<OrgMembersTable
|
|
||||||
isLoading={isOrgUserLoading || IsWsMembershipLoading}
|
|
||||||
isMoreUserNotAllowed={isMoreUsersNotAllowed}
|
|
||||||
orgName={currentOrg?.name || ""}
|
|
||||||
members={orgUsers}
|
|
||||||
workspaceMemberships={workspaceMemberships}
|
|
||||||
onInviteMember={onAddUserToOrg}
|
|
||||||
userId={user?._id || ""}
|
|
||||||
onRemoveMember={onRemoveUserOrgMembership}
|
|
||||||
onRoleChange={onUpdateOrgUserRole}
|
|
||||||
onGrantAccess={onGrantUserAccess}
|
|
||||||
completeInviteLink={completeInviteLink}
|
|
||||||
setCompleteInviteLink={setcompleteInviteLink}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-6 mt-2 w-full rounded-md bg-white/5 p-6">
|
|
||||||
<p className="mr-4 mb-4 text-xl font-semibold text-white">Service Accounts</p>
|
|
||||||
<OrgServiceAccountsTable />
|
<OrgServiceAccountsTable />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
|
||||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
|
||||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
|
||||||
<p className="mb-3 min-w-max text-xl font-semibold">
|
|
||||||
{t("section.incident.incident-contacts")}
|
|
||||||
</p>
|
|
||||||
<p className="mb-2 min-w-max text-xs text-gray-500">
|
|
||||||
{t("section.incident.incident-contacts-description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<OrgIncidentContactsTable
|
|
||||||
isLoading={IsIncidentContactLoading}
|
|
||||||
contacts={incidentContact}
|
|
||||||
onRemoveContact={onRemoveIncidentContact}
|
|
||||||
onAddContact={onAddIncidentContact}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
{/* <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">
|
<p className="text-xl font-bold text-red">
|
||||||
Danger Zone
|
Danger Zone
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-4 text-md text-gray-400">
|
<p className="mt-4 text-md text-gray-400">
|
||||||
As soon as you delete an organization, you will
|
As soon as you delete an organization, you will
|
||||||
not be able to undo it. This will immediately
|
not be able to undo it. This will immediately
|
||||||
remove all organization members and cancel your
|
remove all organization members and cancel your
|
||||||
subscription. If you still want to do that,
|
subscription. If you still want to do that,
|
||||||
please enter the name of the organization below.
|
please enter the name of the organization below.
|
||||||
</p>
|
</p>
|
||||||
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
||||||
<InputField
|
<InputField
|
||||||
label="Organization to be Deleted"
|
label="Organization to be Deleted"
|
||||||
onChangeHandler={
|
onChangeHandler={
|
||||||
setWorkspaceToBeDeletedName
|
setWorkspaceToBeDeletedName
|
||||||
}
|
}
|
||||||
type="varName"
|
type="varName"
|
||||||
value={workspaceToBeDeletedName}
|
value={workspaceToBeDeletedName}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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}
|
onClick={executeDeletingWorkspace}
|
||||||
>
|
>
|
||||||
Delete Project
|
Delete Project
|
||||||
</button>
|
</button>
|
||||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||||
Note: You can only delete a project in case you
|
Note: You can only delete a project in case you
|
||||||
have more than one.
|
have more than one.
|
||||||
</p>
|
</p>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useAddIncidentContact
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const addContactFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label("Email").trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["addContact"]>;
|
||||||
|
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddOrgIncidentContactModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpClose,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: serverDetails } = useFetchServerStatus()
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset
|
||||||
|
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAddIncidentContact();
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ email }: TAddContactForm) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
orgId: currentOrg._id,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully added incident contact",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serverDetails?.emailConfigured){
|
||||||
|
handlePopUpClose("addContact");
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to add incident contact",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addContact?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addContact", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add an Incident Contact"
|
||||||
|
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Add Incident Contact
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addContact")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
|
||||||
|
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
|
||||||
|
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
||||||
|
|
||||||
|
export const OrgIncidentContactsSection = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addContact"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<div className="flex justify-between mb-8">
|
||||||
|
<p className="min-w-max text-xl font-semibold">
|
||||||
|
{t("section.incident.incident-contacts")}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("addContact")}
|
||||||
|
>
|
||||||
|
Add contact
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<OrgIncidentContactsTable />
|
||||||
|
<AddOrgIncidentContactModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
faContactBook,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useDeleteIncidentContact,
|
||||||
|
useGetOrgIncidentContact} from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const OrgIncidentContactsTable = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
|
||||||
|
const [searchContact, setSearchContact] = useState("");
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"removeContact",
|
||||||
|
"setUpEmail"
|
||||||
|
] as const);
|
||||||
|
const { mutateAsync } = useDeleteIncidentContact();
|
||||||
|
|
||||||
|
const onRemoveIncidentContact = async () => {
|
||||||
|
try {
|
||||||
|
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||||
|
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
await mutateAsync({
|
||||||
|
orgId: currentOrg._id,
|
||||||
|
email: incidentContactEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed incident contact",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("removeContact");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove incident contact",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredContacts = contacts ? contacts.filter(({ email }) =>
|
||||||
|
email.toLocaleLowerCase().includes(searchContact)
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={searchContact}
|
||||||
|
onChange={(e) => setSearchContact(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search incident contact by email..."
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
||||||
|
{filteredContacts?.map(({ email }) => (
|
||||||
|
<Tr key={email}>
|
||||||
|
<Td className="w-full">{email}</Td>
|
||||||
|
<Td className="mr-4">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{filteredContacts?.length === 0 && !isLoading && (
|
||||||
|
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeContact.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this email from incident contact?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeContact", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveIncidentContact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal"
|
||||||
|
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
||||||
-200
@@ -1,200 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
faContactBook,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faPlus,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmailServiceSetupModal,
|
|
||||||
EmptyState,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TableSkeleton,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
|
||||||
import { IncidentContact } from "@app/hooks/api/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isLoading?: boolean;
|
|
||||||
contacts?: IncidentContact[];
|
|
||||||
onRemoveContact: (email: string) => Promise<void>;
|
|
||||||
onAddContact: (email: string) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addContactFormSchema = yup.object({
|
|
||||||
email: yup.string().email().required().label("Email").trim()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
|
||||||
|
|
||||||
export const OrgIncidentContactsTable = ({
|
|
||||||
contacts = [],
|
|
||||||
onAddContact,
|
|
||||||
onRemoveContact,
|
|
||||||
isLoading
|
|
||||||
}: Props) => {
|
|
||||||
const [searchContact, setSearchContact] = useState("");
|
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addContact",
|
|
||||||
"removeContact",
|
|
||||||
"setUpEmail"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
|
||||||
|
|
||||||
const onAddIncidentContact = ({ email }: TAddContactForm) => {
|
|
||||||
onAddContact(email);
|
|
||||||
handlePopUpClose("addContact");
|
|
||||||
reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveIncidentContact = async () => {
|
|
||||||
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
|
||||||
await onRemoveContact(incidentContactEmail);
|
|
||||||
handlePopUpClose("removeContact");
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredContacts = contacts.filter(({ email }) =>
|
|
||||||
email.toLocaleLowerCase().includes(searchContact)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mb-4 flex">
|
|
||||||
<div className="mr-4 flex-1">
|
|
||||||
<Input
|
|
||||||
value={searchContact}
|
|
||||||
onChange={(e) => setSearchContact(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
placeholder="Search incident contact by email..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
if (serverDetails?.emailConfigured){
|
|
||||||
handlePopUpOpen("addContact");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("setUpEmail");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Contact
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Email</Th>
|
|
||||||
<Th aria-label="actions" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
|
||||||
{filteredContacts?.map(({ email }) => (
|
|
||||||
<Tr key={email}>
|
|
||||||
<Td className="w-full">{email}</Td>
|
|
||||||
<Td className="mr-4">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{filteredContacts?.length === 0 && !isLoading && (
|
|
||||||
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addContact?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addContact", isOpen);
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Add an Incident Contact"
|
|
||||||
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onAddIncidentContact)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="email"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Add Incident Contact
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpClose("addContact")}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeContact.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this email from incident contact?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeContact", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveIncidentContact}
|
|
||||||
/>
|
|
||||||
<EmailServiceSetupModal
|
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useAddUserToOrg
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const addMemberFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label("Email").trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>; // TODO: change to FormData
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["addMember"]>;
|
||||||
|
handlePopUpClose: (popUpName: keyof UsePopUpState<["addMember"]>) => void;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: test no-SMTP setup case
|
||||||
|
|
||||||
|
export const AddOrgMemberModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpToggle,
|
||||||
|
handlePopUpClose
|
||||||
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
|
const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>("");
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset
|
||||||
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAddUserToOrg();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isInviteLinkCopied) {
|
||||||
|
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isInviteLinkCopied]);
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(completeInviteLink as string);
|
||||||
|
setInviteLinkCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ email }: TAddMemberForm) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
const { data } = await mutateAsync({
|
||||||
|
organizationId: currentOrg?._id,
|
||||||
|
inviteeEmail: email
|
||||||
|
});
|
||||||
|
|
||||||
|
setCompleteInviteLink(data?.completeInviteLink);
|
||||||
|
|
||||||
|
if (!data.completeInviteLink) {
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully sent an invite to the user.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverDetails?.emailConfigured){
|
||||||
|
handlePopUpClose("addMember");
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to send an invite to the user",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addMember", isOpen);
|
||||||
|
setCompleteInviteLink(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={`Invite others to ${currentOrg?.name ?? ""}`}
|
||||||
|
subTitle={
|
||||||
|
<div>
|
||||||
|
{!completeInviteLink && <div>
|
||||||
|
An invite is specific to an email address and expires after 1 day.
|
||||||
|
<br />
|
||||||
|
For security reasons, you will need to separately add members to projects.
|
||||||
|
</div>}
|
||||||
|
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!completeInviteLink && (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} >
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addMember")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{completeInviteLink && (
|
||||||
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{completeInviteLink}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyTokenToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
|
||||||
|
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||||
|
import { OrgMembersTable } from "./OrgMembersTable";
|
||||||
|
|
||||||
|
export const OrgMembersSection = () => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addMember",
|
||||||
|
"upgradePlan",
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const isMoreMembersAllowed = subscription?.memberLimit ? (subscription.membersUsed < subscription.memberLimit) : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<div className="flex justify-between mb-8">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
|
Organization members
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
if (isMoreMembersAllowed) {
|
||||||
|
handlePopUpOpen("addMember");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<OrgMembersTable />
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="Add more members by upgrading to a higher Infisical plan."
|
||||||
|
/>
|
||||||
|
<AddOrgMemberModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+141
-180
@@ -1,20 +1,19 @@
|
|||||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmailServiceSetupModal, EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
@@ -25,73 +24,63 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
UpgradePlanModal} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { useOrganization, useUser, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useAddUserToOrg,
|
||||||
|
useDeleteOrgMembership,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetUserWorkspaceMemberships,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useUpdateOrgUserRole,
|
||||||
|
useUploadWsKey
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { OrgUser, Workspace } from "@app/hooks/api/types";
|
|
||||||
|
|
||||||
type Props = {
|
export const OrgMembersTable = () => {
|
||||||
members?: OrgUser[];
|
|
||||||
workspaceMemberships?: Record<string, Workspace[]>;
|
|
||||||
orgName: string;
|
|
||||||
isLoading?: boolean;
|
|
||||||
isMoreUserNotAllowed: boolean;
|
|
||||||
onRemoveMember: (userId: string) => Promise<void>;
|
|
||||||
onInviteMember: (email: string) => Promise<void>;
|
|
||||||
onRoleChange: (membershipId: string, role: string) => Promise<void>;
|
|
||||||
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
|
||||||
// the current user id to block remove org button
|
|
||||||
userId: string;
|
|
||||||
completeInviteLink: string | undefined,
|
|
||||||
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
|
||||||
email: yup.string().email().required().label("Email").trim()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
|
||||||
|
|
||||||
export const OrgMembersTable = ({
|
|
||||||
members = [],
|
|
||||||
workspaceMemberships = {},
|
|
||||||
orgName,
|
|
||||||
isMoreUserNotAllowed,
|
|
||||||
onRemoveMember,
|
|
||||||
onInviteMember,
|
|
||||||
onGrantAccess,
|
|
||||||
onRoleChange,
|
|
||||||
userId,
|
|
||||||
isLoading,
|
|
||||||
completeInviteLink,
|
|
||||||
setCompleteInviteLink
|
|
||||||
}: Props) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
const { data: serverDetails } = useFetchServerStatus()
|
||||||
|
const { data: members, isLoading: isOrgUserLoading } = useGetOrgUsers(currentOrg?._id ?? ""); // members
|
||||||
|
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(currentOrg?._id ?? "");
|
||||||
|
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || "");
|
||||||
|
|
||||||
|
const uploadWsKey = useUploadWsKey();
|
||||||
|
const addUserToOrg = useAddUserToOrg();
|
||||||
|
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||||
|
const updateOrgUserRole = useUpdateOrgUserRole();
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addMember",
|
|
||||||
"removeMember",
|
"removeMember",
|
||||||
"upgradePlan",
|
|
||||||
"setUpEmail"
|
"setUpEmail"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const {
|
const isLoading = isOrgUserLoading || IsWsMembershipLoading;
|
||||||
control,
|
const userId = currentUser?._id || "";
|
||||||
handleSubmit,
|
const onRemoveMember = async (membershipId: string) => {
|
||||||
reset,
|
if (!currentOrg?._id) return;
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
|
||||||
|
|
||||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
try {
|
||||||
await onInviteMember(email);
|
if (!currentOrg?._id) return;
|
||||||
if (serverDetails?.emailConfigured){
|
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
|
||||||
handlePopUpClose("addMember");
|
createNotification({
|
||||||
}
|
text: "Successfully removed user from org",
|
||||||
|
type: "success"
|
||||||
reset();
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove user from the organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveOrgMemberApproved = async () => {
|
const onRemoveOrgMemberApproved = async () => {
|
||||||
@@ -100,62 +89,111 @@ export const OrgMembersTable = ({
|
|||||||
handlePopUpClose("removeMember");
|
handlePopUpClose("removeMember");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onInviteMember = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await addUserToOrg.mutateAsync({
|
||||||
|
organizationId: currentOrg?._id,
|
||||||
|
inviteeEmail: email
|
||||||
|
});
|
||||||
|
|
||||||
|
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
|
||||||
|
if (!data.completeInviteLink) {
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully invited user to the organization.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to invite user to org",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGrantAccess = async (targetUserId: string, publicKey: string) => {
|
||||||
|
try {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
if (!PRIVATE_KEY || !wsKey) return;
|
||||||
|
|
||||||
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: wsKey.encryptedKey,
|
||||||
|
nonce: wsKey.nonce,
|
||||||
|
publicKey: wsKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadWsKey.mutateAsync({
|
||||||
|
userId: targetUserId,
|
||||||
|
nonce,
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
workspaceId: currentWorkspace?._id || ""
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to grant access to user",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRoleChange = async (membershipId: string, role: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated user role",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update user role",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isIamOwner = useMemo(
|
const isIamOwner = useMemo(
|
||||||
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
|
() => members ? members.find(({ user }) => userId === user?._id)?.role === "owner" : [],
|
||||||
[userId, members]
|
[userId, members]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterdUser = useMemo(
|
const filterdUser = useMemo(
|
||||||
() =>
|
() =>
|
||||||
members.filter(
|
members ? members.filter(
|
||||||
({ user, inviteEmail }) =>
|
({ user, inviteEmail }) =>
|
||||||
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
inviteEmail?.includes(searchMemberFilter)
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
),
|
) : [],
|
||||||
[members, searchMemberFilter]
|
[members, searchMemberFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
if (isInviteLinkCopied) {
|
|
||||||
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
|
|
||||||
}
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isInviteLinkCopied]);
|
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(completeInviteLink as string);
|
|
||||||
setInviteLinkCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4 flex">
|
<Input
|
||||||
<div className="mr-4 flex-1">
|
|
||||||
<Input
|
|
||||||
value={searchMemberFilter}
|
value={searchMemberFilter}
|
||||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search members..."
|
placeholder="Search members..."
|
||||||
/>
|
/>
|
||||||
</div>
|
<TableContainer className="mt-4">
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
if (isMoreUserNotAllowed) {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("addMember");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -167,7 +205,7 @@ export const OrgMembersTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={5} key="org-members" />}
|
{isLoading && <TableSkeleton columns={5} />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
||||||
@@ -213,7 +251,7 @@ export const OrgMembersTable = ({
|
|||||||
<Td>
|
<Td>
|
||||||
{userWs ? (
|
{userWs ? (
|
||||||
userWs?.map(({ name: wsName, _id }) => (
|
userWs?.map(({ name: wsName, _id }) => (
|
||||||
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
<Tag key={`user-${currentUser._id}-workspace-${_id}`} className="my-1">
|
||||||
{wsName}
|
{wsName}
|
||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
@@ -254,74 +292,6 @@ export const OrgMembersTable = ({
|
|||||||
<EmptyState title="No project members found" icon={faUsers} />
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addMember?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addMember", isOpen);
|
|
||||||
setCompleteInviteLink(undefined)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title={`Invite others to ${orgName}`}
|
|
||||||
subTitle={
|
|
||||||
<div>
|
|
||||||
{!completeInviteLink && <div>
|
|
||||||
An invite is specific to an email address and expires after 1 day.
|
|
||||||
<br />
|
|
||||||
For security reasons, you will need to separately add members to projects.
|
|
||||||
</div>}
|
|
||||||
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="email"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpClose("addMember")}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>}
|
|
||||||
{
|
|
||||||
completeInviteLink &&
|
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{completeInviteLink}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeMember.isOpen}
|
isOpen={popUp.removeMember.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
@@ -329,15 +299,6 @@ export const OrgMembersTable = ({
|
|||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
onDeleteApproved={onRemoveOrgMemberApproved}
|
onDeleteApproved={onRemoveOrgMemberApproved}
|
||||||
/>
|
/>
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
|
||||||
/>
|
|
||||||
<EmailServiceSetupModal
|
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersSection } from "./OrgMembersSection";
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { OrgMembersTable } from "./OrgMembersTable";
|
|
||||||
+4
-4
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ const formSchema = yup.object({
|
|||||||
type FormData = yup.InferType<typeof formSchema>;
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
export const OrgNameChangeSection = (): JSX.Element => {
|
export const OrgNameChangeSection = (): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const {
|
const {
|
||||||
@@ -54,9 +52,11 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 "
|
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||||
>
|
>
|
||||||
<p className="text-xl font-semibold text-mineshaft-100 mb-8">{t("common.display-name")}</p>
|
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||||
|
Organization name
|
||||||
|
</p>
|
||||||
<div className="mb-2 max-w-md">
|
<div className="mb-2 max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
|
|||||||
+124
-116
@@ -1,5 +1,8 @@
|
|||||||
import { useEffect, useMemo,useState } from "react";
|
import { useEffect, useMemo,useState } from "react";
|
||||||
import { Controller,useForm } from "react-hook-form";
|
import {
|
||||||
|
// Controller,
|
||||||
|
// useForm
|
||||||
|
} from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
@@ -10,21 +13,21 @@ import {
|
|||||||
faServer,
|
faServer,
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { generateKeyPair } from "@app/components/utilities/cryptography/crypto";
|
// import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
// import * as yup from "yup";
|
||||||
|
// import { generateKeyPair } from "@app/components/utilities/cryptography/crypto";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
// FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
// Select,
|
||||||
SelectItem,
|
// SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -37,25 +40,26 @@ import {
|
|||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceAccount,
|
// useCreateServiceAccount,
|
||||||
useDeleteServiceAccount,
|
useDeleteServiceAccount,
|
||||||
useGetServiceAccounts} from "@app/hooks/api";
|
useGetServiceAccounts
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const serviceAccountExpiration = [
|
// const serviceAccountExpiration = [
|
||||||
{ label: "1 Day", value: 86400 },
|
// { label: "1 Day", value: 86400 },
|
||||||
{ label: "7 Days", value: 604800 },
|
// { label: "7 Days", value: 604800 },
|
||||||
{ label: "1 Month", value: 2592000 },
|
// { label: "1 Month", value: 2592000 },
|
||||||
{ label: "6 months", value: 15552000 },
|
// { label: "6 months", value: 15552000 },
|
||||||
{ label: "12 months", value: 31104000 },
|
// { label: "12 months", value: 31104000 },
|
||||||
{ label: "Never", value: -1 }
|
// { label: "Never", value: -1 }
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const addServiceAccountFormSchema = yup.object({
|
// const addServiceAccountFormSchema = yup.object({
|
||||||
name: yup.string().required().label("Name").trim(),
|
// name: yup.string().required().label("Name").trim(),
|
||||||
expiresIn: yup.string().required().label("Service Account Expiration")
|
// expiresIn: yup.string().required().label("Service Account Expiration")
|
||||||
});
|
// });
|
||||||
|
|
||||||
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||||
|
|
||||||
export const OrgServiceAccountsTable = () => {
|
export const OrgServiceAccountsTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -67,9 +71,9 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||||
const [accessKey, setAccessKey] = useState("");
|
const [accessKey] = useState("");
|
||||||
const [publicKey, setPublicKey] = useState("");
|
const [publicKey] = useState("");
|
||||||
const [privateKey, setPrivateKey] = useState("");
|
const [privateKey] = useState("");
|
||||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addServiceAccount",
|
"addServiceAccount",
|
||||||
@@ -78,7 +82,7 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
|
|
||||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
||||||
|
|
||||||
const createServiceAccount = useCreateServiceAccount();
|
// const createServiceAccount = useCreateServiceAccount();
|
||||||
const removeServiceAccount = useDeleteServiceAccount();
|
const removeServiceAccount = useDeleteServiceAccount();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,32 +102,32 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
handleSubmit,
|
// handleSubmit,
|
||||||
reset,
|
// reset,
|
||||||
formState: { isSubmitting }
|
// formState: { isSubmitting }
|
||||||
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||||
|
|
||||||
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||||
if (!currentOrg?._id) return;
|
// if (!currentOrg?._id) return;
|
||||||
|
|
||||||
const keyPair = generateKeyPair();
|
// const keyPair = generateKeyPair();
|
||||||
setPublicKey(keyPair.publicKey);
|
// setPublicKey(keyPair.publicKey);
|
||||||
setPrivateKey(keyPair.privateKey);
|
// setPrivateKey(keyPair.privateKey);
|
||||||
|
|
||||||
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||||
name,
|
// name,
|
||||||
organizationId: currentOrg?._id,
|
// organizationId: currentOrg?._id,
|
||||||
publicKey: keyPair.publicKey,
|
// publicKey: keyPair.publicKey,
|
||||||
expiresIn: Number(expiresIn)
|
// expiresIn: Number(expiresIn)
|
||||||
});
|
// });
|
||||||
|
|
||||||
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||||
|
|
||||||
setStep(1);
|
// setStep(1);
|
||||||
reset();
|
// reset();
|
||||||
}
|
// }
|
||||||
|
|
||||||
const onRemoveServiceAccount = async () => {
|
const onRemoveServiceAccount = async () => {
|
||||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||||
@@ -144,63 +148,67 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
switch (stepToRender) {
|
switch (stepToRender) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
<div>
|
||||||
<Controller
|
We are currently revising the service account mechanism. In the meantime,
|
||||||
control={control}
|
please use service tokens or API key to fetch secrets via API request.
|
||||||
defaultValue=""
|
</div>
|
||||||
name="name"
|
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||||
render={({ field, fieldState: { error } }) => (
|
// <Controller
|
||||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
// control={control}
|
||||||
<Input {...field} />
|
// defaultValue=""
|
||||||
</FormControl>
|
// name="name"
|
||||||
)}
|
// render={({ field, fieldState: { error } }) => (
|
||||||
/>
|
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<Controller
|
// <Input {...field} />
|
||||||
control={control}
|
// </FormControl>
|
||||||
name="expiresIn"
|
// )}
|
||||||
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
// />
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
// <Controller
|
||||||
return (
|
// control={control}
|
||||||
<FormControl
|
// name="expiresIn"
|
||||||
label="Expiration"
|
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||||
errorText={error?.message}
|
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
isError={Boolean(error)}
|
// return (
|
||||||
>
|
// <FormControl
|
||||||
<Select
|
// label="Expiration"
|
||||||
defaultValue={field.value}
|
// errorText={error?.message}
|
||||||
{...field}
|
// isError={Boolean(error)}
|
||||||
onValueChange={(e) => onChange(e)}
|
// >
|
||||||
className="w-full"
|
// <Select
|
||||||
>
|
// defaultValue={field.value}
|
||||||
{serviceAccountExpiration.map(({ label, value }) => (
|
// {...field}
|
||||||
<SelectItem value={String(value)} key={label}>
|
// onValueChange={(e) => onChange(e)}
|
||||||
{label}
|
// className="w-full"
|
||||||
</SelectItem>
|
// >
|
||||||
))}
|
// {serviceAccountExpiration.map(({ label, value }) => (
|
||||||
</Select>
|
// <SelectItem value={String(value)} key={label}>
|
||||||
</FormControl>
|
// {label}
|
||||||
);
|
// </SelectItem>
|
||||||
}}
|
// ))}
|
||||||
/>
|
// </Select>
|
||||||
<div className="mt-8 flex items-center">
|
// </FormControl>
|
||||||
<Button
|
// );
|
||||||
className="mr-4"
|
// }}
|
||||||
size="sm"
|
// />
|
||||||
type="submit"
|
// <div className="mt-8 flex items-center">
|
||||||
isLoading={isSubmitting}
|
// <Button
|
||||||
isDisabled={isSubmitting}
|
// className="mr-4"
|
||||||
>
|
// size="sm"
|
||||||
Create Service Account
|
// type="submit"
|
||||||
</Button>
|
// isLoading={isSubmitting}
|
||||||
<Button
|
// isDisabled={isSubmitting}
|
||||||
colorSchema="secondary"
|
// >
|
||||||
variant="plain"
|
// Create Service Account
|
||||||
onClick={() => handlePopUpClose("addServiceAccount")}
|
// </Button>
|
||||||
>
|
// <Button
|
||||||
Cancel
|
// colorSchema="secondary"
|
||||||
</Button>
|
// variant="plain"
|
||||||
</div>
|
// onClick={() => handlePopUpClose("addServiceAccount")}
|
||||||
</form>
|
// >
|
||||||
|
// Cancel
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
@@ -269,27 +277,27 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4 flex">
|
<div className="flex justify-between mb-8">
|
||||||
<div className="mr-4 flex-1">
|
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||||
<Input
|
|
||||||
value={searchServiceAccountFilter}
|
|
||||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
placeholder="Search service accounts..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStep(0);
|
setStep(0);
|
||||||
reset();
|
// reset();
|
||||||
handlePopUpOpen("addServiceAccount");
|
handlePopUpOpen("addServiceAccount");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Service Account
|
Add Service Account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer>
|
<Input
|
||||||
|
value={searchServiceAccountFilter}
|
||||||
|
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search service accounts..."
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
@@ -345,7 +353,7 @@ export const OrgServiceAccountsTable = () => {
|
|||||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("addServiceAccount", isOpen);
|
handlePopUpToggle("addServiceAccount", isOpen);
|
||||||
reset();
|
// reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
||||||
export { OrgMembersTable } from "./OrgMembersTable";
|
export { OrgMembersSection } from "./OrgMembersSection";
|
||||||
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
||||||
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { AddAPIKeyModal } from "./AddAPIKeyModal";
|
||||||
|
import { APIKeyTable } from "./APIKeyTable";
|
||||||
|
|
||||||
|
export const APIKeySection = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"addAPIKey"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<div className="flex justify-between mb-8">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">
|
||||||
|
{t("settings.personal.api-keys.title")}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("addAPIKey")}
|
||||||
|
>
|
||||||
|
Add API Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<APIKeyTable />
|
||||||
|
<AddAPIKeyModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
useDeleteAPIKey,
|
||||||
|
useGetMyAPIKeys} from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const APIKeyTable = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { data, isLoading } = useGetMyAPIKeys();
|
||||||
|
const { mutateAsync } = useDeleteAPIKey();
|
||||||
|
|
||||||
|
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
||||||
|
try {
|
||||||
|
await mutateAsync(apiKeyDataId);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted API key",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete API key",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateToFormat: string) => {
|
||||||
|
const date = new Date(dateToFormat);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
|
||||||
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer className="">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="flex-1">Name</Th>
|
||||||
|
<Th className="flex-1">Last active</Th>
|
||||||
|
<Th className="flex-1">Created</Th>
|
||||||
|
<Th className="flex-1">Expiration</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} key="api-keys" />}
|
||||||
|
{!isLoading && data && data.length > 0 && data.map(({
|
||||||
|
_id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
expiresAt,
|
||||||
|
lastUsed
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`api-key-${_id}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
|
<Td>{formatDate(expiresAt)}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteAPIKeyDataClick(_id);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No API Keys on file"
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useCreateAPIKey } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const expirations = [
|
||||||
|
{ label: "1 day", value: "1d" },
|
||||||
|
{ label: "7 days", value: "7d" },
|
||||||
|
{ label: "1 month", value: "1mo" },
|
||||||
|
{ label: "6 months", value: "6mo" },
|
||||||
|
{ label: "12 months", value: "12mo" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const expirationMapping: { [key: string]: number } = {
|
||||||
|
"1d": 86400,
|
||||||
|
"7d": 604800,
|
||||||
|
"1mo": 2592000,
|
||||||
|
"6mo": 15552000,
|
||||||
|
"12mo": 31104000
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
name: yup.string().required("API Key name is required"),
|
||||||
|
expiresIn: yup.string().required("API Key expiration window is required")
|
||||||
|
}).required();
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["addAPIKey"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addAPIKey"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddAPIKeyModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const [newAPIKey, setNewAPIKey] = useState("");
|
||||||
|
const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false);
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { mutateAsync, isLoading } = useCreateAPIKey();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (isAPIKeyCopied) {
|
||||||
|
timer = setTimeout(() => setIsAPIKeyCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [setIsAPIKeyCopied]);
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(newAPIKey);
|
||||||
|
setIsAPIKeyCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, expiresIn }: FormData) => {
|
||||||
|
try {
|
||||||
|
const { apiKey } = await mutateAsync({
|
||||||
|
name,
|
||||||
|
expiresIn: expirationMapping[expiresIn]
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewAPIKey(apiKey);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created API key",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create API key",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAPIKey = Boolean(newAPIKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addAPIKey?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addAPIKey", isOpen);
|
||||||
|
reset();
|
||||||
|
setNewAPIKey("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title="Create API Key">
|
||||||
|
{!hasAPIKey ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="My API Key"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue="6mo"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Expiration"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expirations.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{newAPIKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyTokenToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Click to copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
export { APIKeySection } from "./APIKeySection";
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import ListBox from "@app/components/basic/Listbox";
|
||||||
|
|
||||||
|
export const ChangeLanguageSection = () => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||||
|
{t("settings.personal.change-language")}
|
||||||
|
</p>
|
||||||
|
<div className="w-28">
|
||||||
|
<ListBox
|
||||||
|
isSelected={lang}
|
||||||
|
onChange={setLanguage}
|
||||||
|
data={["en", "ko", "fr", "es"]}
|
||||||
|
text={`${t("common.language")}: `}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ChangeLanguageSection } from "./ChangeLanguageSection";
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
|
||||||
|
import checkPassword from "@app/components/utilities/checks/checkPassword";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { useGetCommonPasswords } from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Errors = {
|
||||||
|
length?: string,
|
||||||
|
upperCase?: string,
|
||||||
|
lowerCase?: string,
|
||||||
|
number?: string,
|
||||||
|
specialChar?: string,
|
||||||
|
repeatedChar?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
oldPassword: yup.string().required("Old password is required"),
|
||||||
|
newPassword: yup.string().required("New password is required")
|
||||||
|
}).required();
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
export const ChangePasswordSection = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { data: commonPasswords } = useGetCommonPasswords();
|
||||||
|
const { reset, control, handleSubmit } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
oldPassword: "",
|
||||||
|
newPassword: ""
|
||||||
|
},
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!user?.email) return;
|
||||||
|
if (!commonPasswords) return;
|
||||||
|
|
||||||
|
const errorCheck = checkPassword({
|
||||||
|
password: newPassword,
|
||||||
|
commonPasswords,
|
||||||
|
setErrors
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorCheck) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
await attemptChangePassword({
|
||||||
|
email: user.email,
|
||||||
|
currentPassword: oldPassword,
|
||||||
|
newPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully changed password",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
window.location.href = "/login";
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setIsLoading(false);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to change password",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
|
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||||
|
Change password
|
||||||
|
</h2>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input
|
||||||
|
placeholder="Old password"
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
className="bg-mineshaft-800"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="oldPassword"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input
|
||||||
|
placeholder="New password"
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
className="bg-mineshaft-800"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="newPassword"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{Object.keys(errors).length > 0 && (
|
||||||
|
<div className="my-4 max-w-md flex 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>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ChangePasswordSection } from "./ChangePasswordSection";
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
password: yup.string().required("Password is required")
|
||||||
|
}).required();
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
export const EmergencyKitSection = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { reset, control, handleSubmit } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = ({
|
||||||
|
password
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!user?.email) return;
|
||||||
|
|
||||||
|
issueBackupKey({
|
||||||
|
email: user.email,
|
||||||
|
password,
|
||||||
|
personalName: `${user.firstName} ${user.lastName}`,
|
||||||
|
setBackupKeyError: () => {},
|
||||||
|
setBackupKeyIssued: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to download emergency kit",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
|
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100">
|
||||||
|
Emergency Kit
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 mb-8">
|
||||||
|
The kit contains information you can use to recover your account.
|
||||||
|
</p>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
className="bg-mineshaft-800"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={false}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EmergencyKitSection } from "./EmergencyKitSection";
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import { APIKeySection } from "../APIKeySection";
|
||||||
|
|
||||||
|
export const PersonalAPIKeyTab = () => {
|
||||||
|
return (
|
||||||
|
<APIKeySection />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalAPIKeyTab } from "./PersonalAPIKeyTab";
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||||
|
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||||
|
import { SecuritySection } from "../SecuritySection";
|
||||||
|
import { SessionsSection } from "../SessionsSection";
|
||||||
|
|
||||||
|
export const PersonalSecurityTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SecuritySection />
|
||||||
|
<SessionsSection />
|
||||||
|
<ChangePasswordSection />
|
||||||
|
<EmergencyKitSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalSecurityTab } from "./PersonalSecurityTab";
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
|
|
||||||
|
import { PersonalTabGroup } from "./PersonalTabGroup";
|
||||||
|
|
||||||
|
export const PersonalSettingsPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||||
|
<div className="max-w-screen-lg w-full">
|
||||||
|
<NavHeader pageName={t("settings.personal.title")} isProjectRelated={false} />
|
||||||
|
<div className="my-8">
|
||||||
|
<p className="text-3xl font-semibold text-gray-200">
|
||||||
|
{t("settings.personal.title")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PersonalTabGroup />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import { Fragment } from "react"
|
||||||
|
import { Tab } from "@headlessui/react"
|
||||||
|
|
||||||
|
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
|
||||||
|
import { PersonalSecurityTab } from "../PersonalSecurityTab";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: "Security", key: "tab-account-security" },
|
||||||
|
{ name: "API Keys", key: "tab-account-api-keys" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PersonalTabGroup = () => {
|
||||||
|
return (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab as={Fragment} key={tab.key}>
|
||||||
|
{({ selected }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-30 p-4 font-semibold outline-none ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"}`}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel>
|
||||||
|
<PersonalSecurityTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<PersonalAPIKeyTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalTabGroup } from "./PersonalTabGroup";
|
||||||
+2
-2
@@ -51,8 +51,8 @@ export const SecuritySection = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form>
|
<form>
|
||||||
<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="p-4 mb-6 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
<p className="mb-4 mt-2 text-xl font-semibold">
|
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||||
Two-factor Authentication
|
Two-factor Authentication
|
||||||
</p>
|
</p>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useRevokeMySessions } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { SessionsTable } from "./SessionsTable";
|
||||||
|
|
||||||
|
export const SessionsSection = () => {
|
||||||
|
const { mutateAsync } = useRevokeMySessions();
|
||||||
|
|
||||||
|
const onRevokeAllSessionsClick = async () => {
|
||||||
|
try {
|
||||||
|
await mutateAsync();
|
||||||
|
window.location.href = "/login";
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 mb-6 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<div className="flex justify-between mb-8">
|
||||||
|
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100">
|
||||||
|
Sessions
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faBan} />}
|
||||||
|
onClick={onRevokeAllSessionsClick}
|
||||||
|
>
|
||||||
|
Revoke all
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 mb-8">
|
||||||
|
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>
|
||||||
|
<SessionsTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useGetMySessions } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const SessionsTable = () => {
|
||||||
|
const { data, isLoading } = useGetMySessions();
|
||||||
|
|
||||||
|
const formatDate = (dateToFormat: string) => {
|
||||||
|
const date = new Date(dateToFormat);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
|
||||||
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Created</Th>
|
||||||
|
<Th>Last active</Th>
|
||||||
|
<Th>IP address</Th>
|
||||||
|
<Th>Device</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} key="sesssions" />}
|
||||||
|
{!isLoading && data && data.length > 0 && data.map(({
|
||||||
|
_id,
|
||||||
|
createdAt,
|
||||||
|
lastUsed,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`session-${_id}`}>
|
||||||
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
|
<Td>{ip}</Td>
|
||||||
|
<Td>{userAgent}</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<EmptyState
|
||||||
|
title="No sessions on file"
|
||||||
|
icon={faServer}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SessionsSection } from "./SessionsSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalSettingsPage } from "./PersonalSettingsPage";
|
||||||
@@ -356,7 +356,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
|
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
|
||||||
<div className="relative right-5">
|
<div className="relative right-5 ml-4">
|
||||||
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
||||||
</div>
|
</div>
|
||||||
<div className="my-8 flex max-w-5xl flex-row items-center justify-between text-xl">
|
<div className="my-8 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||||
|
|||||||
Reference in New Issue
Block a user