mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 10:18:16 +00:00
Finish preliminary making user settings, org settings styling similar to usage and billing page
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<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">
|
||||
<p className="mb-4 mt-2 text-xl font-semibold">
|
||||
<div className="p-4 mb-6 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||
Two-factor Authentication
|
||||
</p>
|
||||
<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";
|
||||
Reference in New Issue
Block a user