mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 22:29:33 +00:00
Add frontend, backend and CLI
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import Plan from "../../../components/billing/Plan";
|
||||
import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription"
|
||||
import getOrganizationUsers from "../../api/organization/GetOrgUsers"
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
|
||||
|
||||
export default function SettingsBilling() {
|
||||
let [currentPlan, setCurrentPlan] = useState("");
|
||||
let [numUsers, setNumUsers] = useState("");
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 1,
|
||||
name: "Starter",
|
||||
price: "Free",
|
||||
priceExplanation: "up to 5 team members",
|
||||
text: "Manage any project with 5 members for free!",
|
||||
subtext: "$5 per member/month afterwards.",
|
||||
buttonTextMain: "Downgrade",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: currentPlan == process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: "Professional",
|
||||
price: "$9",
|
||||
priceExplanation: "/member/month",
|
||||
subtext: "Includes unlimited projects & members.",
|
||||
text: "Keep up with key management as you grow.",
|
||||
buttonTextMain: "Upgrade",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: currentPlan == process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO,
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: "Enterprise",
|
||||
price: "Custom Pricing",
|
||||
text: "More control for advanced organizations.",
|
||||
buttonTextMain: "Schedule a Demo",
|
||||
buttonTextSecondary: "Learn More",
|
||||
current: false,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(async () => {
|
||||
const subscriptions = await getOrganizationSubscriptions({orgId: localStorage.getItem("orgData.id")});
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
const orgUsers = await getOrganizationUsers({orgId: localStorage.getItem("orgData.id")})
|
||||
setNumUsers(orgUsers.length)
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings - Billing</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Usage & Billing"/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
||||
<div className="flex flex-col justify-start items-start text-3xl">
|
||||
<p className="font-semibold mr-4 text-gray-200">
|
||||
Usage & Billing
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your organization's subscription here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
||||
<p className="text-xl font-semibold">Subscription</p>
|
||||
<div className="flex flex-row mt-4 overflow-x-auto">
|
||||
{plans.map((plan) => (
|
||||
<Plan
|
||||
key={plan.name}
|
||||
plan={plan}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className='text-xl font-bold mt-12'>Current Usage</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">{numUsers}</p>
|
||||
<p className="text-gray-300">{numUsers > 1 ? "Organization members" : "Organization member"}</p>
|
||||
</div>
|
||||
{/* <div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">1 </p>
|
||||
<p className="text-gray-300">Organization projects</p>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsBilling.requireAuth = true;
|
||||
@@ -0,0 +1,390 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMagnifyingGlass, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
||||
import AddIncidentContactDialog from "../../../components/basic/dialog/AddIncidentContactDialog";
|
||||
import getIncidentContacts from "../../api/organization/getIncidentContacts";
|
||||
import deleteIncidentContact from "../../api/organization/deleteIncidentContact";
|
||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
||||
import AddUserDialog from "../../../components/basic/dialog/AddUserDialog";
|
||||
import UserTable from "../../../components/basic/table/UserTable";
|
||||
import getUser from "../../api/user/getUser";
|
||||
import guidGenerator from "../../../components/utilities/randomId";
|
||||
import addUserToOrg from "../../api/organization/addUserToOrg";
|
||||
import getOrganizationUsers from "../../api/organization/GetOrgUsers";
|
||||
import renameOrg from "../../api/organization/renameOrg";
|
||||
import getOrganization from "../../api/organization/GetOrg";
|
||||
import getOrganizationSubscriptions from "../../api/organization/GetOrgSubscription";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [orgName, setOrgName] = useState("");
|
||||
const [emailUser, setEmailUser] = useState("");
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] =
|
||||
useState("");
|
||||
const [searchUsers, setSearchUsers] = useState("");
|
||||
const [workspaceId, setWorkspaceId] = useState("");
|
||||
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] =
|
||||
useState(false);
|
||||
const [isAddUserOpen, setIsAddUserOpen] = useState(
|
||||
router.asPath.split("?")[1] == "invite"
|
||||
);
|
||||
const [incidentContacts, setIncidentContacts] = useState([]);
|
||||
const [searchIncidentContact, setSearchIncidentContact] = useState("");
|
||||
const [userList, setUserList] = useState();
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
let workspaceIdTemp;
|
||||
const [email, setEmail] = useState("");
|
||||
const [currentPlan, setCurrentPlan] = useState("");
|
||||
|
||||
useEffect(async () => {
|
||||
let org = await getOrganization({
|
||||
orgId: localStorage.getItem("orgData.id"),
|
||||
});
|
||||
let orgData = org;
|
||||
setOrgName(orgData.name);
|
||||
let incidentContactsData = await getIncidentContacts(localStorage.getItem("orgData.id"));
|
||||
|
||||
setIncidentContacts(
|
||||
incidentContactsData?.map(
|
||||
(contact) => contact.email
|
||||
)
|
||||
);
|
||||
|
||||
const user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
|
||||
workspaceIdTemp = router.query.id;
|
||||
let orgUsers = await getOrganizationUsers({
|
||||
orgId: localStorage.getItem("orgData.id"),
|
||||
});
|
||||
setUserList(
|
||||
orgUsers.map((user) => ({
|
||||
key: guidGenerator(),
|
||||
firstName: user.user?.firstName,
|
||||
lastName: user.user?.lastName,
|
||||
email:
|
||||
user.user?.email == null
|
||||
? user.inviteEmail
|
||||
: user.user?.email,
|
||||
role: user?.role,
|
||||
status: user?.status,
|
||||
userId: user.user?._id,
|
||||
membershipId: user._id,
|
||||
publicKey: user.user?.publicKey,
|
||||
}))
|
||||
);
|
||||
const subscriptions = await getOrganizationSubscriptions({orgId: localStorage.getItem("orgData.id")});
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
}, []);
|
||||
|
||||
const modifyOrgName = (newName) => {
|
||||
setButtonReady(true);
|
||||
setOrgName(newName);
|
||||
};
|
||||
|
||||
const submitChanges = (newOrgName) => {
|
||||
renameOrg(localStorage.getItem("orgData.id"), newOrgName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
setWorkspaceId(router.query.id);
|
||||
}, []);
|
||||
|
||||
function closeAddUserModal() {
|
||||
setIsAddUserOpen(false);
|
||||
}
|
||||
|
||||
function closeAddIncidentContactModal() {
|
||||
setIsAddIncidentContactOpen(false);
|
||||
}
|
||||
|
||||
function openAddUserModal() {
|
||||
setIsAddUserOpen(true);
|
||||
}
|
||||
|
||||
function openAddIncidentContactModal() {
|
||||
setIsAddIncidentContactOpen(true);
|
||||
}
|
||||
|
||||
async function submitAddUserModal(email) {
|
||||
await addUserToOrg(email, localStorage.getItem("orgData.id"));
|
||||
setEmail("");
|
||||
setIsAddUserOpen(false);
|
||||
router.reload();
|
||||
}
|
||||
|
||||
const deleteIncidentContactFully = (incidentContact) => {
|
||||
setIncidentContacts(
|
||||
incidentContacts.filter((contact) => contact != incidentContact)
|
||||
);
|
||||
deleteIncidentContact(localStorage.getItem("orgData.id"), incidentContact);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function deleted a workspace.
|
||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||
*/
|
||||
const executeDeletingWorkspace = async () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
|
||||
if (userWorkspaces.length > 1) {
|
||||
if (
|
||||
userWorkspaces.filter(
|
||||
(workspace) => workspace._id == router.query.id
|
||||
)[0].name == workspaceToBeDeletedName
|
||||
) {
|
||||
await deleteWorkspace(router.query.id);
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Organization Settings"/>
|
||||
<AddIncidentContactDialog
|
||||
isOpen={isAddIncidentContactOpen}
|
||||
closeModal={closeAddIncidentContactModal}
|
||||
workspaceId={workspaceId}
|
||||
incidentContacts={incidentContacts}
|
||||
setIncidentContacts={setIncidentContacts}
|
||||
/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
||||
<div className="flex flex-col justify-start items-start text-3xl">
|
||||
<p className="font-semibold mr-4 text-gray-200">
|
||||
Organization Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your organization here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
|
||||
<div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
|
||||
Display Name
|
||||
</p>
|
||||
<InputField
|
||||
// label="Organization Name"
|
||||
onChangeHandler={modifyOrgName}
|
||||
type="varName"
|
||||
value={orgName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-4 mb-2`}
|
||||
>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={() => submitChanges(orgName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<p className="font-semibold mr-4 text-white text-xl">
|
||||
Organization Members
|
||||
</p>
|
||||
<p className="mr-4 text-gray-400 mt-2 mb-2">
|
||||
Manage members of your organization. These users
|
||||
could afterwards be formed into projects.
|
||||
</p>
|
||||
<AddUserDialog
|
||||
isOpen={isAddUserOpen}
|
||||
closeModal={closeAddUserModal}
|
||||
submitModal={submitAddUserModal}
|
||||
email={emailUser}
|
||||
workspaceId={workspaceId}
|
||||
setEmail={setEmailUser}
|
||||
currentPlan={currentPlan}
|
||||
orgName={orgName}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="pb-1 w-full flex flex-row items-start max-w-6xl">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
||||
icon={faMagnifyingGlass}
|
||||
/>
|
||||
<input
|
||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
||||
value={searchUsers}
|
||||
onChange={(e) =>
|
||||
setSearchUsers(e.target.value)
|
||||
}
|
||||
placeholder={"Search members..."}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
text="Add Member"
|
||||
onButtonPressed={openAddUserModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{userList && (
|
||||
<div className="overflow-y-auto max-w-6xl w-full">
|
||||
<UserTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers.toLowerCase()}
|
||||
resendInvite={submitAddUserModal}
|
||||
isOrg={true}
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
||||
Incident Contacts
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-2 min-w-max">
|
||||
These contacts will be notified in the
|
||||
unlikely event of a severe incident.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-end justify-center">
|
||||
<Button
|
||||
text="Add Contact"
|
||||
onButtonPressed={openAddIncidentContactModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flex flwex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400"
|
||||
icon={faMagnifyingGlass}
|
||||
/>
|
||||
<input
|
||||
className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none"
|
||||
value={searchIncidentContact}
|
||||
onChange={(e) =>
|
||||
setSearchIncidentContact(e.target.value)
|
||||
}
|
||||
placeholder={"Search..."}
|
||||
/>
|
||||
</div>
|
||||
{incidentContacts?.filter((email) =>
|
||||
email.includes(searchIncidentContact)
|
||||
).length > 0 ? (
|
||||
incidentContacts
|
||||
.filter((email) =>
|
||||
email.includes(searchIncidentContact)
|
||||
)
|
||||
.map((contact) => (
|
||||
<div key={guidGenerator()} className="flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full">
|
||||
<p className="text-gray-300">
|
||||
{contact}
|
||||
</p>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
deleteIncidentContactFully(
|
||||
contact
|
||||
)
|
||||
}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="w-full flex flex-row justify-center mt-6 max-w-4xl ml-6">
|
||||
<p className="text-gray-400">
|
||||
No incident contacts found.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
||||
<p className="text-xl font-bold text-red">
|
||||
Danger Zone
|
||||
</p>
|
||||
<p className="mt-4 text-md text-gray-400">
|
||||
As soon as you delete an organization, you will
|
||||
not be able to undo it. This will immediately
|
||||
remove all organization members and cancel your
|
||||
subscription. If you still want to do that,
|
||||
please enter the name of the organization below.
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
||||
<InputField
|
||||
label="Organization to be Deleted"
|
||||
onChangeHandler={
|
||||
setWorkspaceToBeDeletedName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceToBeDeletedName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={executeDeletingWorkspace}
|
||||
>
|
||||
Delete Project
|
||||
</button>
|
||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||
Note: You can only delete a project in case you
|
||||
have more than one.
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsOrg.requireAuth = true;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getUser from "../../api/user/getUser";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import passwordCheck from "../../../components/utilities/checks/PasswordCheck";
|
||||
import changePassword from "../../../components/utilities/changePassword";
|
||||
import issueBackupKey from "../../../components/utilities/issueBackupKey";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
|
||||
|
||||
export default function PersonalSettings() {
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
const [personalName, setPersonalName] = useState("");
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
const [currentPasswordError, setCurrentPasswordError] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [backupPassword, setBackupPassword] = useState("");
|
||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
let user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
setPersonalName(user.firstName + " " + user.lastName);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Personal Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Personal Settings" isProjectRelated={false}/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 text-xl max-w-5xl">
|
||||
<div className="flex flex-col justify-start items-start text-3xl">
|
||||
<p className="font-semibold mr-4 text-gray-200">
|
||||
Personal Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
View and manage your personal information here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md flex flex-col items-end pb-4">
|
||||
{/* <div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
|
||||
Display Name
|
||||
</p>
|
||||
<InputField
|
||||
onChangeHandler={modifyOrgName}
|
||||
type="varName"
|
||||
value={orgName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-4 mb-2 rounded-md bg-gray-800 text-sm ${
|
||||
buttonReady &&
|
||||
"hover:bg-primary hover:text-black hover:text-semibold duration-200 cursor-pointer"
|
||||
} text-gray-400 px-4 py-2.5`}
|
||||
>
|
||||
{buttonReady ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-start justify-center font-medium px-2"
|
||||
onClick={() =>
|
||||
submitChanges(orgName)
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-row items-center jutify-center px-4">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg mr-3 text-gray-400"
|
||||
icon={faCheck}
|
||||
/>
|
||||
<p className="font-base">
|
||||
Saved
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
||||
Change password
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-xl w-full">
|
||||
<InputField
|
||||
label="Current Password"
|
||||
onChangeHandler={(password) => {
|
||||
setCurrentPassword(password);
|
||||
}}
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
isRequired
|
||||
error={currentPasswordError}
|
||||
errorText="The current password may be wrong"
|
||||
/>
|
||||
<div className="py-2"></div>
|
||||
<InputField
|
||||
label="New Password"
|
||||
onChangeHandler={(password) => {
|
||||
setNewPassword(password);
|
||||
passwordCheck(
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
false
|
||||
);
|
||||
}}
|
||||
type="password"
|
||||
value={newPassword}
|
||||
isRequired
|
||||
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
|
||||
/>
|
||||
</div>
|
||||
{(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber) ? <div className="w-full mt-3 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md max-w-xl mb-2">
|
||||
<div className={`text-gray-400 text-sm mb-1`}>Password should contain at least:</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLength ? "text-gray-400" : "text-gray-600"} text-sm`}>14 characters</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"} text-sm`}>1 lowercase character</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber
|
||||
? <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5"/>
|
||||
: <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2"/>}
|
||||
<div className={`${passwordErrorNumber ? "text-gray-400" : "text-gray-600"} text-sm`}>1 number</div>
|
||||
</div>
|
||||
</div> : <div className="py-2"></div>}
|
||||
<div className="flex flex-row items-center mt-3 w-52 pr-3">
|
||||
<Button
|
||||
text="Change Password"
|
||||
onButtonPressed={() => {
|
||||
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) {
|
||||
changePassword(
|
||||
personalEmail,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
setCurrentPasswordError,
|
||||
setPasswordChanged,
|
||||
setCurrentPassword,
|
||||
setNewPassword
|
||||
)
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={(newPassword != "") && (currentPassword != "") && !(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)}
|
||||
textDisabled="Change Password"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className={`ml-4 text-primary text-3xl ${passwordChanged ? "opacity-100" : "opacity-0"} duration-300`}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 mt-4 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
||||
Emergency Kit
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300 min-w-max">
|
||||
Your Emergency Kit contains the information you’ll need to sign in to your Infisical account.
|
||||
</p>
|
||||
<p className="text-sm text-mineshaft-300 mb-5 min-w-max">
|
||||
Only the latest issued Emergency Kit remains valid. To get a new Emergency Kit, verify your password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-xl mb-4">
|
||||
<InputField
|
||||
label="Current Password"
|
||||
onChangeHandler={setBackupPassword}
|
||||
type="password"
|
||||
value={backupPassword}
|
||||
isRequired
|
||||
error={backupKeyError}
|
||||
errorText="The current password is wrong"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center mt-3 w-full w-60">
|
||||
<Button
|
||||
text="Download Emergency Kit"
|
||||
onButtonPressed={() => {
|
||||
issueBackupKey({
|
||||
email: personalEmail,
|
||||
password: backupPassword,
|
||||
personalName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={backupPassword != ""}
|
||||
textDisabled="Download Emergency Kit"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className={`ml-4 text-primary text-3xl ${backupKeyIssued ? "opacity-100" : "opacity-0"} duration-300`}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PersonalSettings.requireAuth = true;
|
||||
@@ -0,0 +1,281 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
|
||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "../../../components/basic/InputField";
|
||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
||||
import renameWorkspace from "../../api/workspace/renameWorkspace";
|
||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
||||
import NavHeader from "../../../components/navigation/NavHeader";
|
||||
import Button from "../../../components/basic/buttons/Button";
|
||||
import ServiceTokenTable from "../../../components/basic/table/ServiceTokenTable";
|
||||
import getServiceTokens from "../../api/serviceToken/getServiceTokens"
|
||||
import AddServiceTokenDialog from "../../../components/basic/dialog/AddServiceTokenDialog";
|
||||
|
||||
export default function SettingsBasic() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceName, setWorkspaceName] = useState("");
|
||||
const [serviceTokens, setServiceTokens] = useState([]);
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] =
|
||||
useState("");
|
||||
const [workspaceId, setWorkspaceId] = useState("");
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
userWorkspaces.map((userWorkspace) => {
|
||||
if (userWorkspace._id == router.query.id) {
|
||||
setWorkspaceName(userWorkspace.name);
|
||||
}
|
||||
});
|
||||
let tempServiceTokens = await getServiceTokens({workspaceId: router.query.id});
|
||||
setServiceTokens(tempServiceTokens);
|
||||
}, []);
|
||||
|
||||
const modifyWorkspaceName = (newName) => {
|
||||
setButtonReady(true);
|
||||
setWorkspaceName(newName);
|
||||
};
|
||||
|
||||
const submitChanges = (newWorkspaceName) => {
|
||||
renameWorkspace(router.query.id, newWorkspaceName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
setWorkspaceId(router.query.id);
|
||||
}, []);
|
||||
|
||||
function closeAddModal() {
|
||||
setIsAddOpen(false);
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
setIsAddOpen(true);
|
||||
}
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
setIsAddServiceTokenDialogOpen(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function deleted a workspace.
|
||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||
*/
|
||||
const executeDeletingWorkspace = async () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
|
||||
if (userWorkspaces.length > 1) {
|
||||
if (
|
||||
userWorkspaces.filter(
|
||||
(workspace) => workspace._id == router.query.id
|
||||
)[0].name == workspaceToBeDeletedName
|
||||
) {
|
||||
await deleteWorkspace(router.query.id);
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Settings</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<AddServiceTokenDialog
|
||||
isOpen={isAddServiceTokenDialogOpen}
|
||||
workspaceId={router.query.id}
|
||||
closeModal={closeAddServiceTokenModal}
|
||||
workspaceName={workspaceName}
|
||||
/>
|
||||
<div className="flex flex-row mr-6 max-w-5xl">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName="Project Settings" isProjectRelated={true}/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
||||
<div className="flex flex-col justify-start items-start text-3xl">
|
||||
<p className="font-semibold mr-4 text-gray-200">
|
||||
Project Settings
|
||||
</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
These settings only apply to the currently
|
||||
selected Project.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-start">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
|
||||
<p className="text-xl font-semibold mb-4">
|
||||
Display Name
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<InputField
|
||||
onChangeHandler={
|
||||
modifyWorkspaceName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-4 mb-2`}
|
||||
>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={() => submitChanges(workspaceName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
Project ID
|
||||
</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start mt-4">
|
||||
To integrate Infisical into your code
|
||||
base and get automatic injection of
|
||||
environmental variables, you should use
|
||||
the following Project ID.
|
||||
</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start">
|
||||
For more guidance, including code
|
||||
snipets for various languages and
|
||||
frameworks, see{" "}
|
||||
<a
|
||||
href="https://infisical.com/docs/gettingStarted"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:opacity-80 duration-200"
|
||||
>
|
||||
Infisical Docs.
|
||||
</a>
|
||||
</p>
|
||||
<div className="max-h-28 w-ful">
|
||||
<InputField
|
||||
type="varName"
|
||||
value={router.query.id}
|
||||
placeholder=""
|
||||
isRequired
|
||||
static
|
||||
text="This is your project's auto-generated unique identifier. It can't be changed."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">
|
||||
Service Tokens
|
||||
</p>
|
||||
<p className="text-base text-gray-400 mb-4">
|
||||
Every service token is specific to you, a certain project and a certain environment within this project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<Button
|
||||
text="Add New Token"
|
||||
onButtonPressed={() => {setIsAddServiceTokenDialogOpen(true)}}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ServiceTokenTable data={serviceTokens} workspaceName={workspaceName}/>
|
||||
</div>
|
||||
|
||||
{/* <div className="bg-white/5 rounded-md px-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-6 pt-6">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
Project Environments
|
||||
</p>
|
||||
<p className="text-md mr-1 text-gray-400 mt-2 self-start">
|
||||
Choose which environments will show up
|
||||
in your Dashboard. Some common ones
|
||||
include Development, Staging, and
|
||||
Production. Often, teams choose to add
|
||||
Testing.
|
||||
</p>
|
||||
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||
Note: the text in brackets shows how
|
||||
these environmant should be accessed in
|
||||
CLI.
|
||||
</p>
|
||||
<div className="rounded-md h-10 w-full mr-auto mt-4 flex flex-row">
|
||||
{envOptions.map((env) => (
|
||||
<div className="bg-white/5 hover:bg-white/10 duration-200 h-full w-max px-3 flex flex-row items-center justify-between rounded-md mr-1 text-sm">
|
||||
{env}
|
||||
<XIcon
|
||||
className="h-5 w-5 ml-2 mt-0.5 text-white cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="group bg-white/5 hover:bg-primary hover:text-black duration-200 h-full w-max py-1 px-3 flex flex-row items-center justify-between rounded-md mr-1 cursor-pointer text-sm font-semibold">
|
||||
<PlusIcon
|
||||
className="h-5 w-5 text-white mr-2 group-hover:text-black"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
||||
<p className="text-xl font-bold text-red">
|
||||
Danger Zone
|
||||
</p>
|
||||
<p className="mt-2 text-md text-gray-400">
|
||||
As soon as you delete this project, you will not
|
||||
be able to undo it. This will immediately remove
|
||||
all the keys. If you still want to do that,
|
||||
please enter the name of the project below.
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
|
||||
<InputField
|
||||
label="Project to be Deleted"
|
||||
onChangeHandler={
|
||||
setWorkspaceToBeDeletedName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceToBeDeletedName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={executeDeletingWorkspace}
|
||||
>
|
||||
Delete Project
|
||||
</button>
|
||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||
Note: You can only delete a project in case you
|
||||
have more than one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsBasic.requireAuth = true;
|
||||
Reference in New Issue
Block a user