mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 12:29:31 +00:00
Merge branch 'Infisical:main' into cloudflare-workers
This commit is contained in:
@@ -2,7 +2,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import issueBackupKey from "../utilities/cryptography/issueBackupKey";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { generateUserBackupKey } from "@app/lib/crypto";
|
||||
|
||||
import { useNotificationContext } from "../context/Notifications/NotificationProvider";
|
||||
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
|
||||
import { Button } from "../v2";
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
@@ -28,35 +32,56 @@ export default function DonwloadBackupPDFStep({
|
||||
name
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
const handleBackupKeyGenerate = async () => {
|
||||
try {
|
||||
setIsLoading.on();
|
||||
const generatedKey = await generateUserBackupKey(email, password);
|
||||
await generateBackupPDFAsync({
|
||||
generatedKey,
|
||||
personalEmail: email,
|
||||
personalName: name
|
||||
});
|
||||
incrementStep();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Faield to generate backup key"
|
||||
});
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
|
||||
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t("signup.step4-message")}
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
|
||||
{t("signup.step4-message")}
|
||||
</p>
|
||||
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
|
||||
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
|
||||
<span className='mb-2'>{t("signup.step4-description1")} {t("signup.step4-description3")}</span>
|
||||
<span className="mb-2">
|
||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: name,
|
||||
setBackupKeyError: () => { },
|
||||
setBackupKeyIssued: () => { }
|
||||
});
|
||||
incrementStep();
|
||||
}}
|
||||
onClick={handleBackupKeyGenerate}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
className="h-12"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
> Download PDF </Button>
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,6 @@ import Telemetry from "./telemetry/Telemetry";
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
import SecurityClient from "./SecurityClient";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
interface IsLoginSuccessful {
|
||||
mfaEnabled: boolean;
|
||||
success: boolean;
|
||||
@@ -22,118 +19,100 @@ interface IsLoginSuccessful {
|
||||
* @param {string} email - email of user to log in
|
||||
* @param {string} password - password of user to log in
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
{
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
): Promise<IsLoginSuccessful> => {
|
||||
const attemptLogin = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}): Promise<IsLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
const { serverPublicKey, salt } = await login1({
|
||||
email,
|
||||
clientPublicKey,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
const {
|
||||
mfaEnabled,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
token,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag
|
||||
} = await login2(
|
||||
{
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken,
|
||||
}
|
||||
);
|
||||
|
||||
if (mfaEnabled) {
|
||||
// case: MFA is enabled
|
||||
|
||||
// set temporary (MFA) JWT token
|
||||
SecurityClient.setMfaToken(token);
|
||||
|
||||
resolve({
|
||||
mfaEnabled,
|
||||
success: true
|
||||
});
|
||||
} else if (
|
||||
!mfaEnabled &&
|
||||
encryptionVersion &&
|
||||
encryptedPrivateKey &&
|
||||
iv &&
|
||||
tag &&
|
||||
token
|
||||
) {
|
||||
// case: MFA is not enabled
|
||||
|
||||
// unset provider auth token in case it was used
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email, email);
|
||||
telemetry.capture("User Logged In");
|
||||
}
|
||||
|
||||
resolve({
|
||||
mfaEnabled: false,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
await new Promise((resolve) => {
|
||||
client.init({ username: email, password }, () => resolve(null));
|
||||
});
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
const { serverPublicKey, salt } = await login1({
|
||||
email,
|
||||
clientPublicKey,
|
||||
providerAuthToken
|
||||
});
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
const {
|
||||
mfaEnabled,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
token,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag
|
||||
} = await login2({
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken
|
||||
});
|
||||
|
||||
if (mfaEnabled) {
|
||||
// case: MFA is enabled
|
||||
|
||||
// set temporary (MFA) JWT token
|
||||
SecurityClient.setMfaToken(token);
|
||||
|
||||
return {
|
||||
mfaEnabled,
|
||||
success: true
|
||||
};
|
||||
}
|
||||
if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
|
||||
// case: MFA is not enabled
|
||||
|
||||
// unset provider auth token in case it was used
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email, email);
|
||||
telemetry.capture("User Logged In");
|
||||
}
|
||||
|
||||
return {
|
||||
mfaEnabled: false,
|
||||
success: true
|
||||
};
|
||||
}
|
||||
return { success: false, mfaEnabled: false };
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetchOrgUsers,fetchUserAction } from "@app/hooks/api/users/queries";
|
||||
import { fetchOrgUsers, fetchUserAction } from "@app/hooks/api/users/queries";
|
||||
|
||||
interface OnboardingCheckProps {
|
||||
orgId: string;
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
setHasUserClickedSlack?: (value: boolean) => void;
|
||||
setHasUserClickedIntro?: (value: boolean) => void;
|
||||
@@ -12,6 +13,7 @@ interface OnboardingCheckProps {
|
||||
* This function checks which onboarding steps a user has already finished.
|
||||
*/
|
||||
const onboardingCheck = async ({
|
||||
orgId,
|
||||
setTotalOnboardingActionsDone,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserClickedIntro,
|
||||
@@ -19,9 +21,7 @@ const onboardingCheck = async ({
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
let countActions = 0;
|
||||
const userActionSlack = await fetchUserAction(
|
||||
"slack_cta_clicked"
|
||||
);
|
||||
const userActionSlack = await fetchUserAction("slack_cta_clicked");
|
||||
|
||||
if (userActionSlack) {
|
||||
countActions += 1;
|
||||
@@ -41,9 +41,8 @@ const onboardingCheck = async ({
|
||||
}
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id");
|
||||
const orgUsers = await fetchOrgUsers(orgId || "");
|
||||
|
||||
|
||||
if (orgUsers.length > 1) {
|
||||
countActions += 1;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const yyyy = today.getFullYear();
|
||||
|
||||
const todayFormatted = `${mm}/${dd}/${yyyy}`;
|
||||
|
||||
|
||||
function createPdfHeader(doc: jsPDF, personalName: string) {
|
||||
doc.setFillColor(255, 255, 255);
|
||||
doc.rect(0, 0, 600, 900, "F");
|
||||
@@ -92,5 +91,15 @@ function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFPro
|
||||
doc.save("Infisical Emergency Kit.pdf");
|
||||
}
|
||||
|
||||
export default generateBackupPDF;
|
||||
/**
|
||||
* This function generate a pdf with a secret key for a user.
|
||||
*/
|
||||
export function generateBackupPDFAsync({ personalName, personalEmail, generatedKey }: PDFProps) {
|
||||
// eslint-disable-next-line new-cap
|
||||
const doc = new jsPDF("p", "pt", "a4", true);
|
||||
createPdfHeader(doc, personalName);
|
||||
createPdfContent(doc, personalEmail, generatedKey);
|
||||
return doc.save("Infisical Emergency Kit.pdf", { returnPromise: true });
|
||||
}
|
||||
|
||||
export default generateBackupPDF;
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
text?: string | string[];
|
||||
frequency?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
|
||||
export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
|
||||
const [pos, setPos] = useState(0);
|
||||
const isTextArray = Array.isArray(text);
|
||||
useEffect(() => {
|
||||
@@ -23,7 +25,12 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex relative flex-col h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8">
|
||||
<div
|
||||
className={twMerge(
|
||||
"container mx-auto flex relative flex-col h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
{text && isTextArray && (
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disabled" | "required"> & {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
id: string;
|
||||
isChecked?: boolean;
|
||||
isRequired?: boolean;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
@@ -16,38 +14,36 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||
const link = (subscription && subscription.slug !== null)
|
||||
? `/org/${currentOrg?._id}/billing`
|
||||
: "https://infisical.com/scheduledemo";
|
||||
|
||||
const link =
|
||||
subscription && subscription.slug !== null
|
||||
? `/org/${currentOrg?._id}/billing`
|
||||
: "https://infisical.com/scheduledemo";
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
|
||||
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// direct user to upgrade their plan
|
||||
window.location.href = link;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Unleash Infisical's Full Power"
|
||||
>
|
||||
<ModalContent title="Unleash Infisical's Full Power">
|
||||
<p className="mb-2 text-bunker-300">{text}</p>
|
||||
<p className="text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
@@ -59,10 +55,10 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
onClick={handleUpgradeBtnClick}
|
||||
className="mr-4"
|
||||
>
|
||||
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
{subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => onOpenChange && onOpenChange(false)}
|
||||
>
|
||||
@@ -71,5 +67,5 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,8 @@ export const publicPaths = [
|
||||
"/saml-sso",
|
||||
"/login/provider/success", // TODO: change
|
||||
"/login/provider/error", // TODO: change
|
||||
"/login/sso"
|
||||
"/login/sso",
|
||||
"/admin/signup"
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
@@ -50,7 +51,8 @@ const plansProd: Mapping = {
|
||||
|
||||
export const plans = plansProd || plansDev;
|
||||
|
||||
export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?";
|
||||
export const leaveConfirmDefaultMessage =
|
||||
"Your changes will be lost if you leave the page. Are you sure you want to continue?";
|
||||
|
||||
export const secretTagsColors = [
|
||||
{
|
||||
@@ -115,5 +117,5 @@ export const secretTagsColors = [
|
||||
rgba: "rgb(255,0,0, 0.8)",
|
||||
name: "Red",
|
||||
selected: false
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -18,7 +18,7 @@ type Props = {
|
||||
// Provide a context for whole app to notify user is authorized or not
|
||||
export const AuthProvider = ({ children }: Props): JSX.Element => {
|
||||
const { isLoading } = useGetAuthToken();
|
||||
const { pathname, push } = useRouter();
|
||||
const { pathname, push, asPath } = useRouter();
|
||||
const [isReady, setIsReady] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,7 +26,7 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
|
||||
if (!isLoading) {
|
||||
// not a public path and not authenticated kick to login page
|
||||
if (!publicPaths.includes(pathname) && !isLoggedIn()) {
|
||||
push("/login").then(() => {
|
||||
push({ pathname: "/login", query: { redirect: asPath } }).then(() => {
|
||||
setIsReady.on();
|
||||
});
|
||||
} else {
|
||||
@@ -40,7 +40,12 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
|
||||
if (isLoading || !isReady) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="infisical loading indicator" />
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { ContentLoader } from "@app/components/v2/ContentLoader";
|
||||
import { useGetServerConfig } from "@app/hooks/api";
|
||||
import { TServerConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
type TServerConfigContext = {
|
||||
config: TServerConfig;
|
||||
};
|
||||
|
||||
const ServerConfigContext = createContext<TServerConfigContext | null>(null);
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ServerConfigProvider = ({ children }: Props): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const { data, isLoading } = useGetServerConfig();
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TServerConfigContext>(() => {
|
||||
return {
|
||||
config: data!
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && data && !data.initialized) {
|
||||
router.push("/admin/signup");
|
||||
}
|
||||
}, [isLoading, data]);
|
||||
|
||||
if (isLoading || (!data?.initialized && router.pathname !== "/admin/signup")) {
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<ContentLoader text="Loading configurations" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
|
||||
};
|
||||
|
||||
export const useServerConfig = () => {
|
||||
const ctx = useContext(ServerConfigContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useServerConfig has to be used within <UserContext.Provider>");
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext";
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "./ProjectPermissionContext";
|
||||
export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext";
|
||||
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
||||
export { UserProvider, useUser } from "./UserContext";
|
||||
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateAdminUser, useUpdateServerConfig } from "./mutation";
|
||||
export { useGetServerConfig } from "./queries";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { User } from "../users/types";
|
||||
import { adminQueryKeys } from "./queries";
|
||||
import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
||||
|
||||
export const useCreateAdminUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ user: User; token: string }, {}, TCreateAdminUserDTO>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.post("/api/v1/admin/signup", opt);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateServerConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TServerConfig, {}, Partial<TServerConfig>>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
|
||||
"/api/v1/admin/config",
|
||||
opt
|
||||
);
|
||||
return data.config;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
|
||||
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TServerConfig } from "./types";
|
||||
|
||||
export const adminQueryKeys = {
|
||||
serverConfig: () => ["server-config"] as const
|
||||
};
|
||||
|
||||
const fetchServerConfig = async () => {
|
||||
const { data } = await apiRequest.get<{ config: TServerConfig }>("/api/v1/admin/config");
|
||||
return data.config;
|
||||
};
|
||||
|
||||
export const useGetServerConfig = ({
|
||||
options = {}
|
||||
}: {
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TServerConfig,
|
||||
unknown,
|
||||
TServerConfig,
|
||||
ReturnType<typeof adminQueryKeys.serverConfig>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>;
|
||||
} = {}) =>
|
||||
useQuery({
|
||||
queryKey: adminQueryKeys.serverConfig(),
|
||||
queryFn: fetchServerConfig,
|
||||
...options,
|
||||
enabled: options?.enabled ?? true
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
export type TServerConfig = {
|
||||
initialized: boolean;
|
||||
allowSignUp: boolean;
|
||||
};
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
protectedKey: string;
|
||||
protectedKeyTag: string;
|
||||
protectedKeyIV: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
publicKey: string;
|
||||
verifier: string;
|
||||
salt: string;
|
||||
};
|
||||
@@ -4,17 +4,6 @@ import {
|
||||
UserAgentType
|
||||
} from "./enums";
|
||||
|
||||
enum Permission {
|
||||
READ = "read",
|
||||
READ_WRITE = "readWrite"
|
||||
}
|
||||
|
||||
interface Scope {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -211,7 +200,7 @@ interface CreateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<Scope>;
|
||||
role: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +210,7 @@ interface UpdateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<Scope>;
|
||||
role?: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +220,7 @@ interface DeleteServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<Scope>;
|
||||
role?: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./apiKeys";
|
||||
export * from "./admin"
|
||||
export * from "./auditLogs";
|
||||
export * from "./auth";
|
||||
export * from "./bots";
|
||||
|
||||
@@ -85,8 +85,8 @@ export const useUpdateServiceTokenV3 = () => {
|
||||
mutationFn: async ({
|
||||
serviceTokenDataId,
|
||||
name,
|
||||
role,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
@@ -94,8 +94,8 @@ export const useUpdateServiceTokenV3 = () => {
|
||||
}) => {
|
||||
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
|
||||
name,
|
||||
role,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Permission } from "./enums";
|
||||
|
||||
export type ServiceTokenScope = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@@ -38,12 +36,6 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
|
||||
|
||||
// --- v3
|
||||
|
||||
export type ServiceTokenV3Scope = {
|
||||
permissions: Permission[];
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type ServiceTokenV3TrustedIp = {
|
||||
_id: string;
|
||||
ipAddress: string;
|
||||
@@ -54,13 +46,17 @@ export type ServiceTokenV3TrustedIp = {
|
||||
export type ServiceTokenDataV3 = {
|
||||
_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
customRole?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
workspace: string;
|
||||
isActive: boolean;
|
||||
refreshTokenLastUsed?: string;
|
||||
accessTokenLastUsed?: string;
|
||||
refreshTokenUsageCount: number;
|
||||
accessTokenUsageCount: number;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
expiresAt?: string;
|
||||
accessTokenTTL: number;
|
||||
@@ -71,9 +67,9 @@ export type ServiceTokenDataV3 = {
|
||||
|
||||
export type CreateServiceTokenDataV3DTO = {
|
||||
name: string;
|
||||
role?: string;
|
||||
workspaceId: string;
|
||||
publicKey: string;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
@@ -93,7 +89,7 @@ export type UpdateServiceTokenDataV3DTO = {
|
||||
serviceTokenDataId: string;
|
||||
isActive?: boolean;
|
||||
name?: string;
|
||||
scopes?: ServiceTokenV3Scope[];
|
||||
role?: string;
|
||||
trustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
|
||||
@@ -14,6 +14,7 @@ export type User = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
email: string;
|
||||
superAdmin: boolean;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
authProvider?: AuthMethod;
|
||||
|
||||
@@ -7,7 +7,6 @@ export {
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetUserWsEnvironments,
|
||||
useGetWorkspaceAuthorizations,
|
||||
useGetWorkspaceById,
|
||||
useGetWorkspaceIndexStatus,
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
CreateWorkspaceDTO,
|
||||
DeleteEnvironmentDTO,
|
||||
DeleteWorkspaceDTO,
|
||||
GetWsEnvironmentDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
RenameWorkspaceDTO,
|
||||
ReorderEnvironmentsDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace,
|
||||
WorkspaceEnv
|
||||
Workspace
|
||||
} from "./types";
|
||||
|
||||
export const workspaceKeys = {
|
||||
@@ -31,7 +29,6 @@ export const workspaceKeys = {
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
|
||||
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceServiceTokenDataV3: (workspaceId: string) =>
|
||||
@@ -103,21 +100,6 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchUserWsEnvironments = async (workspaceId: string) => {
|
||||
const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>(
|
||||
`/api/v2/workspace/${workspaceId}/environments`
|
||||
);
|
||||
return data.accessibleEnvironments;
|
||||
};
|
||||
|
||||
export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId),
|
||||
onSuccess,
|
||||
queryKey: workspaceKeys.getUserWsEnvironments(workspaceId),
|
||||
queryFn: () => fetchUserWsEnvironments(workspaceId)
|
||||
});
|
||||
|
||||
// to get all userids in an org with the workspace they are part of
|
||||
export const useGetUserWorkspaceMemberships = (orgId: string) =>
|
||||
useQuery({
|
||||
|
||||
@@ -30,11 +30,6 @@ export type CreateWorkspaceDTO = {
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export type GetWsEnvironmentDTO = {
|
||||
workspaceId: string;
|
||||
onSuccess?: (data: WorkspaceEnv[]) => void;
|
||||
};
|
||||
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// @ts-nocheck
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faEnvelope,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faMobile,
|
||||
faPlus,
|
||||
faQuestion
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
useGetOrgTrialUrl,
|
||||
useGetUserAction,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction
|
||||
} from "@app/hooks/api";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const supportOptions = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
"https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||
"Read Docs",
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||
"GitHub Issues",
|
||||
"https://github.com/Infisical/infisical/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||
"Email Support",
|
||||
"mailto:[email protected]"
|
||||
]
|
||||
];
|
||||
|
||||
export const AdminLayout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const { data: updateClosed } = useGetUserAction("september_update_closed");
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const registerUserAction = useRegisterUserAction();
|
||||
|
||||
const closeUpdate = async () => {
|
||||
await registerUserAction.mutateAsync("september_update_closed");
|
||||
};
|
||||
|
||||
const logout = useLogoutUser();
|
||||
const logOutUser = async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
{!router.asPath.includes("personal") && (
|
||||
<div className="flex h-12 cursor-default justify-between items-center px-3 pt-6">
|
||||
<Link href={`/org/${currentOrg?._id}/overview`}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
|
||||
>
|
||||
<div
|
||||
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
|
||||
style={{ fontSize: "11px", width: "26px", height: "26px" }}
|
||||
>
|
||||
{user?.firstName?.charAt(0)}
|
||||
{user?.lastName && user?.lastName?.charAt(0)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||
<Link href="/personal-settings">
|
||||
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||
</Link>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
Documentation
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
<a
|
||||
href="https://infisical.com/slack"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
Join Slack Community
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
{user?.superAdmin && (
|
||||
<Link href="/admin" legacyBehavior>
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Admin Panel
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<button type="button" onClick={logOutUser} className="w-full">
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
</button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`relative mt-10 ${
|
||||
subscription && subscription.slug === "starter" && !subscription.has_used_trial
|
||||
? "mb-2"
|
||||
: "mb-4"
|
||||
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
!updateClosed ? "block" : "hidden"
|
||||
} relative z-10 mb-6 flex pb-2 w-52 flex-col items-center justify-start rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3`}
|
||||
>
|
||||
<div className="text-md mt-2 w-full font-semibold text-mineshaft-100">
|
||||
Infisical September update
|
||||
</div>
|
||||
<div className="mt-1 mb-1 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300">
|
||||
Improved RBAC, new integrations, dashboard remake, and more!
|
||||
</div>
|
||||
<div className="mt-2 h-[6.77rem] w-full rounded-md border border-mineshaft-700">
|
||||
<Image
|
||||
src="/images/infisical-update-september-2023.png"
|
||||
height={319}
|
||||
width={539}
|
||||
alt="kubernetes image"
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex w-full items-center justify-between px-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => closeUpdate()}
|
||||
className="text-mineshaft-400 duration-200 hover:text-mineshaft-100"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<a
|
||||
href="https://infisical.com/blog/infisical-update-september-2023"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-normal leading-[1.2rem] text-mineshaft-400 duration-200 hover:text-mineshaft-100"
|
||||
>
|
||||
Learn More{" "}
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-0.5 text-xs" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{router.asPath.includes("org") && (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||
Invite people
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{supportOptions.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{subscription &&
|
||||
subscription.slug === "starter" &&
|
||||
!subscription.has_used_trial && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="mt-1.5 w-full"
|
||||
>
|
||||
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
|
||||
<FontAwesomeIcon
|
||||
icon={faInfinity}
|
||||
className="mr-3 ml-0.5 py-2 text-primary"
|
||||
/>
|
||||
Start Free Pro Trial
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{infisicalPlatformVersion && (
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {infisicalPlatformVersion}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AdminLayout } from "./AdminLayout";
|
||||
@@ -399,6 +399,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
{user?.superAdmin && (
|
||||
<Link href="/admin" legacyBehavior>
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Admin Panel
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<button type="button" onClick={logOutUser} className="w-full">
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
@@ -498,7 +505,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
{t("nav.menu.members")}
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
@@ -541,19 +548,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
||||
}
|
||||
icon="system-outline-126-verified"
|
||||
>
|
||||
IP Allowlist
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link> */}
|
||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { AdminLayout } from "./AdminLayout";
|
||||
export { AppLayout } from "./AppLayout";
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
|
||||
|
||||
export const generateUserBackupKey = async (email: string, password: string) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const clientKey = new jsrp.client();
|
||||
// eslint-disable-next-line new-cap
|
||||
const clientPassword = new jsrp.client();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
clientPassword.init({ username: email, password }, () => resolve(null));
|
||||
});
|
||||
const clientPublicKey = clientPassword.getPublicKey();
|
||||
const srpKeys = await srp1({ clientPublicKey });
|
||||
clientPassword.setSalt(srpKeys.salt);
|
||||
clientPassword.setServerPublicKey(srpKeys.serverPublicKey);
|
||||
|
||||
const clientProof = clientPassword.getProof(); // called M1
|
||||
const generatedKey = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
await new Promise((resolve) => {
|
||||
clientKey.init({ username: email, password: generatedKey }, () => resolve(null));
|
||||
});
|
||||
|
||||
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
|
||||
(resolve, reject) => {
|
||||
clientKey.createVerifier((err, res) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(res);
|
||||
});
|
||||
}
|
||||
);
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: String(localStorage.getItem("PRIVATE_KEY")),
|
||||
secret: generatedKey
|
||||
});
|
||||
|
||||
await issueBackupPrivateKey({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
clientProof
|
||||
});
|
||||
|
||||
return generatedKey;
|
||||
};
|
||||
|
||||
export const generateUserPassKey = async (email: string, password: string) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
client.init({ username: email, password }, () => resolve(null));
|
||||
});
|
||||
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
|
||||
(resolve, reject) => {
|
||||
client.createVerifier((err, res) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(res);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
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);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString("hex"),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
return {
|
||||
protectedKey,
|
||||
protectedKeyTag,
|
||||
protectedKeyIV,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
publicKey,
|
||||
verifier,
|
||||
salt,
|
||||
privateKey
|
||||
};
|
||||
};
|
||||
+29
-22
@@ -20,6 +20,7 @@ import {
|
||||
OrgPermissionProvider,
|
||||
OrgProvider,
|
||||
ProjectPermissionProvider,
|
||||
ServerConfigProvider,
|
||||
SubscriptionProvider,
|
||||
UserProvider,
|
||||
WorkspaceProvider
|
||||
@@ -84,36 +85,42 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
<ServerConfigProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</ServerConfigProvider>
|
||||
</NotificationProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const Layout = Component?.layout || AppLayout;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<AuthProvider>
|
||||
<OrgProvider>
|
||||
<OrgPermissionProvider>
|
||||
<WorkspaceProvider>
|
||||
<ProjectPermissionProvider>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<NotificationProvider>
|
||||
<AppLayout>
|
||||
<Component {...pageProps} />
|
||||
</AppLayout>
|
||||
</NotificationProvider>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
</ProjectPermissionProvider>
|
||||
</WorkspaceProvider>
|
||||
</OrgPermissionProvider>
|
||||
</OrgProvider>
|
||||
</AuthProvider>
|
||||
<NotificationProvider>
|
||||
<ServerConfigProvider>
|
||||
<AuthProvider>
|
||||
<OrgProvider>
|
||||
<OrgPermissionProvider>
|
||||
<WorkspaceProvider>
|
||||
<ProjectPermissionProvider>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
</ProjectPermissionProvider>
|
||||
</WorkspaceProvider>
|
||||
</OrgPermissionProvider>
|
||||
</OrgProvider>
|
||||
</AuthProvider>
|
||||
</ServerConfigProvider>
|
||||
</NotificationProvider>
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { AdminLayout } from "@app/layouts";
|
||||
import { AdminDashboardPage } from "@app/views/admin/DashboardPage";
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("admin.dashboard") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("admin.dashboard.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("admin.dashboard.og-description") ?? ""} />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
<AdminDashboardPage />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
|
||||
AdminDashboard.requireAuth = true;
|
||||
|
||||
AdminDashboard.layout = AdminLayout;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { SignUpPage } from "@app/views/admin/SignUpPage";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen max-h-screen overflow-y-auto flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("signup.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("signup.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("signup.og-description") ?? ""} />
|
||||
</Head>
|
||||
<SignUpPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -559,6 +559,7 @@ const OrganizationPage = withPermission(
|
||||
|
||||
useEffect(() => {
|
||||
onboardingCheck({
|
||||
orgId: currentOrg,
|
||||
setHasUserClickedIntro,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserPushedSecrets,
|
||||
|
||||
@@ -12,6 +12,7 @@ import InitialSignupStep from "@app/components/signup/InitialSignupStep";
|
||||
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
@@ -34,6 +35,13 @@ export default function SignUp() {
|
||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { mutateAsync } = useVerifyEmailVerificationCode();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.allowSignUp) {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [config.allowSignUp]);
|
||||
|
||||
useEffect(() => {
|
||||
const tryAuth = async () => {
|
||||
@@ -65,7 +73,7 @@ export default function SignUp() {
|
||||
const { token } = await mutateAsync({ email, code });
|
||||
SecurityClient.setSignupToken(token);
|
||||
setStep(3);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setCodeError(true);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { useServerConfig } from "@app/context";
|
||||
|
||||
import { navigateUserToOrg } from "../../Login.utils";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { config } = useServerConfig();
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||
@@ -84,7 +84,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await navigateUserToOrg(router);
|
||||
|
||||
// case: login does not require MFA step
|
||||
@@ -225,7 +225,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
</Button>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||
{!serverDetails?.inviteOnlySignup ? (
|
||||
{config.allowSignUp ? (
|
||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||
<Link href="/signup">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
@@ -236,7 +236,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex flex-row text-sm text-bunker-400">
|
||||
<div className="flex flex-row text-sm text-bunker-400 mt-2">
|
||||
<Link href="/verify-email">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Forgot password? Recover your account
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { MemberListTab } from "./components/MemberListTab";
|
||||
import { ProjectRoleListTab } from "./components/ProjectRoleListTab";
|
||||
import { ServiceTokenTab } from "./components/ServiceTokenTab";
|
||||
|
||||
enum TabSections {
|
||||
Member = "members",
|
||||
Roles = "roles"
|
||||
Roles = "roles",
|
||||
ServiceTokens = "service-tokens"
|
||||
}
|
||||
|
||||
export const MembersPage = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const orgId = currentWorkspace?.organization || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
||||
{t("settings.members.title")}
|
||||
Access Control
|
||||
</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
@@ -47,14 +37,14 @@ export const MembersPage = withProjectPermission(
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<MemberListTab roles={roles as TRole<string>[]} isRolesLoading={isRolesLoading} />
|
||||
<MemberListTab />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.ServiceTokens}>
|
||||
<ServiceTokenTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<ProjectRoleListTab
|
||||
roles={roles as TRole<string>[]}
|
||||
isRolesLoading={isRolesLoading}
|
||||
/>
|
||||
<ProjectRoleListTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -46,17 +46,11 @@ import {
|
||||
useAddUserToWs,
|
||||
useDeleteUserFromWorkspace,
|
||||
useGetOrgUsers,
|
||||
useGetRoles,
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceUsers,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUploadWsKey
|
||||
} from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
type Props = {
|
||||
roles?: TRole<string>[];
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
useUploadWsKey} from "@app/hooks/api";
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
@@ -64,7 +58,7 @@ const addMemberFormSchema = z.object({
|
||||
|
||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||
|
||||
export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
export const MemberListTab = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -76,6 +70,11 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
|
||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||
@@ -162,7 +161,7 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
|
||||
const findRoleFromId = useCallback(
|
||||
(roleId: string) => {
|
||||
return roles.find(({ _id: id }) => id === roleId);
|
||||
return (roles || []).find(({ _id: id }) => id === roleId);
|
||||
},
|
||||
[roles]
|
||||
);
|
||||
@@ -307,7 +306,7 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
|
||||
onRoleChange(membershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{roles
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
|
||||
+2
-11
@@ -8,13 +8,8 @@ import { TRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectRoleList } from "./components/ProjectRoleList";
|
||||
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
|
||||
|
||||
type Props = {
|
||||
roles?: TRole<string>[];
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectRoleListTab = withProjectPermission(
|
||||
({ roles = [], isRolesLoading }: Props) => {
|
||||
() => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
|
||||
return popUp.editRole.isOpen ? (
|
||||
@@ -38,11 +33,7 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<ProjectRoleList
|
||||
roles={roles}
|
||||
isRolesLoading={isRolesLoading}
|
||||
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
|
||||
/>
|
||||
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
|
||||
+12
-8
@@ -25,24 +25,26 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRole } from "@app/hooks/api";
|
||||
import { useDeleteRole, useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
type Props = {
|
||||
isRolesLoading?: boolean;
|
||||
roles?: TRole<string>[];
|
||||
onSelectRole: (role?: TRole<string>) => void;
|
||||
};
|
||||
|
||||
export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
|
||||
export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
const [searchRoles, setSearchRoles] = useState("");
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useDeleteRole();
|
||||
|
||||
@@ -62,6 +64,8 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
}
|
||||
};
|
||||
|
||||
// roles={roles as TRole<string>[]}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
@@ -97,7 +101,7 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{(roles as TRole<string>[])?.map((role) => {
|
||||
const { _id: id, name, slug } = role;
|
||||
const isNonMutatable = ["admin", "member", "viewer"].includes(slug);
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import {
|
||||
ServiceTokenSection,
|
||||
// ServiceTokenV3Section
|
||||
} from "./components";
|
||||
|
||||
export const ServiceTokenTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-service-token"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
{/* <ServiceTokenV3Section /> */}
|
||||
<ServiceTokenSection />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
+56
-174
@@ -29,22 +29,17 @@ import {
|
||||
Tabs,
|
||||
UpgradePlanModal} from "@app/components/v2";
|
||||
import {
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useCreateServiceTokenV3,
|
||||
useGetRoles,
|
||||
useGetUserWsKey,
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
Permission
|
||||
} from "@app/hooks/api/serviceTokens/enums";
|
||||
import {
|
||||
ServiceTokenV3Scope,
|
||||
ServiceTokenV3TrustedIp
|
||||
} from "@app/hooks/api/serviceTokens/types";
|
||||
useUpdateServiceTokenV3} from "@app/hooks/api";
|
||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
enum TabSections {
|
||||
@@ -61,13 +56,6 @@ const expirations = [
|
||||
{ label: "12 months", value: "31104000" }
|
||||
];
|
||||
|
||||
const permissionsMap: {
|
||||
[key: string]: Permission[]
|
||||
} = {
|
||||
"read": [Permission.READ],
|
||||
"readWrite": [Permission.READ, Permission.WRITE],
|
||||
}
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required("ST V3 name is required"),
|
||||
expiresIn: yup.string(),
|
||||
@@ -82,24 +70,7 @@ const schema = yup.object({
|
||||
return !Number.isNaN(num) && num > 0 && String(num) === value;
|
||||
})
|
||||
.required("Access Token TTL is required"),
|
||||
scopes: yup
|
||||
.array(
|
||||
yup.object({
|
||||
permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"),
|
||||
environment: yup.string().max(50).required().label("Environment"),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.default("/")
|
||||
.label("Secret Path")
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Scope"),
|
||||
role: yup.string().required("ST V3 role is required"),
|
||||
trustedIps: yup
|
||||
.array(
|
||||
yup.object({
|
||||
@@ -129,9 +100,18 @@ export const AddServiceTokenV3Modal = ({
|
||||
const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { data: roles } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
const { createNotification } = useNotificationContext();
|
||||
@@ -145,11 +125,6 @@ export const AddServiceTokenV3Modal = ({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
accessTokenTTL: "7200",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
secretPath: "/",
|
||||
}],
|
||||
trustedIps: [{
|
||||
ipAddress: "0.0.0.0/0"
|
||||
}]
|
||||
@@ -175,31 +150,22 @@ export const AddServiceTokenV3Modal = ({
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
serviceTokenDataId: string;
|
||||
name: string;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
role: string;
|
||||
customRole: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
accessTokenTTL: number;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
};
|
||||
|
||||
if (!roles?.length) return;
|
||||
|
||||
if (serviceTokenData) {
|
||||
reset({
|
||||
name: serviceTokenData.name,
|
||||
scopes: serviceTokenData.scopes.map(({
|
||||
environment,
|
||||
secretPath,
|
||||
permissions
|
||||
}: ServiceTokenV3Scope) => {
|
||||
let permission = "read";
|
||||
if (permissions.includes(Permission.WRITE)) {
|
||||
permission = "readWrite";
|
||||
}
|
||||
|
||||
return ({
|
||||
environment,
|
||||
secretPath,
|
||||
permission
|
||||
})
|
||||
}),
|
||||
role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role,
|
||||
trustedIps: serviceTokenData.trustedIps.map(({
|
||||
ipAddress,
|
||||
prefix
|
||||
@@ -215,26 +181,21 @@ export const AddServiceTokenV3Modal = ({
|
||||
reset({
|
||||
name: "",
|
||||
accessTokenTTL: "7200",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
secretPath: "/",
|
||||
}],
|
||||
role: roles[0].slug,
|
||||
trustedIps: [{
|
||||
ipAddress: "0.0.0.0/0"
|
||||
}]
|
||||
});
|
||||
}
|
||||
}, [popUp?.serviceTokenV3?.data]);
|
||||
}, [popUp?.serviceTokenV3?.data, roles]);
|
||||
|
||||
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
|
||||
const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" });
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
scopes,
|
||||
role,
|
||||
trustedIps,
|
||||
isRefreshTokenRotationEnabled
|
||||
}: FormData) => {
|
||||
@@ -242,25 +203,16 @@ export const AddServiceTokenV3Modal = ({
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
serviceTokenDataId: string;
|
||||
name: string;
|
||||
scopes: any;
|
||||
role: string;
|
||||
};
|
||||
|
||||
// convert read/readWrite permission => ["read", "write"] format
|
||||
const reformattedScopes = scopes.map((scope) => {
|
||||
return ({
|
||||
environment: scope.environment,
|
||||
secretPath: scope.secretPath,
|
||||
permissions: permissionsMap[scope.permission]
|
||||
});
|
||||
});
|
||||
|
||||
if (serviceTokenData) {
|
||||
// update
|
||||
|
||||
await updateMutateAsync({
|
||||
serviceTokenDataId: serviceTokenData.serviceTokenDataId,
|
||||
name,
|
||||
scopes: reformattedScopes,
|
||||
role,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
@@ -270,7 +222,7 @@ export const AddServiceTokenV3Modal = ({
|
||||
handlePopUpToggle("serviceTokenV3", false);
|
||||
} else {
|
||||
// create
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!workspaceId) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
@@ -294,9 +246,9 @@ export const AddServiceTokenV3Modal = ({
|
||||
|
||||
const { refreshToken } = await createMutateAsync({
|
||||
name,
|
||||
workspaceId: currentWorkspace._id,
|
||||
role,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
scopes: reformattedScopes,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
@@ -385,102 +337,32 @@ export const AddServiceTokenV3Modal = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{tokenScopes.map(({ id }, index) => (
|
||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.permission`}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Permission" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-36"
|
||||
>
|
||||
<SelectItem value="read" key="st-v3-read">
|
||||
Read
|
||||
</SelectItem>
|
||||
<SelectItem value="readWrite" key="st-v3-write">
|
||||
Read & Write
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.environment`}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Environment" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-36"
|
||||
>
|
||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Role`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(roles || []).map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.secretPath`}
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Secrets Path" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => remove(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
append({
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||
secretPath: "/"
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
+13
-27
@@ -22,8 +22,7 @@ import {
|
||||
useGetWorkspaceServiceTokenDataV3,
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import { Permission } from "@app/hooks/api/serviceTokens/enums"
|
||||
import { ServiceTokenV3Scope, ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -32,7 +31,11 @@ type Props = {
|
||||
data?: {
|
||||
serviceTokenDataId?: string;
|
||||
name?: string;
|
||||
scopes?: ServiceTokenV3Scope[];
|
||||
role?: string;
|
||||
customRole?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
||||
accessTokenTTL?: number;
|
||||
isRefreshTokenRotationEnabled?: boolean;
|
||||
@@ -47,7 +50,7 @@ export const ServiceTokenV3Table = ({
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || "");
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
|
||||
|
||||
const handleToggleServiceTokenDataStatus = async ({
|
||||
serviceTokenDataId,
|
||||
isActive
|
||||
@@ -81,7 +84,7 @@ export const ServiceTokenV3Table = ({
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Scopes</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Trusted IPs</Th>
|
||||
<Th>Access Token TTL</Th>
|
||||
<Th>Created At</Th>
|
||||
@@ -98,7 +101,8 @@ export const ServiceTokenV3Table = ({
|
||||
_id,
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
role,
|
||||
customRole,
|
||||
trustedIps,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
@@ -128,26 +132,7 @@ export const ServiceTokenV3Table = ({
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{scopes.map((scope) => {
|
||||
let permissionText = "read"
|
||||
if (
|
||||
scope.permissions.includes(Permission.WRITE) &&
|
||||
scope.permissions.includes(Permission.READ)
|
||||
) {
|
||||
permissionText = "readWrite";
|
||||
}
|
||||
|
||||
return (
|
||||
<p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}>
|
||||
<span className="font-bold">
|
||||
{permissionText}
|
||||
</span>
|
||||
{` @${scope.environment} - ${scope.secretPath}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
<Td>{customRole?.slug ?? role}</Td>
|
||||
<Td>
|
||||
{trustedIps.map(({
|
||||
_id: trustedIpId,
|
||||
@@ -175,7 +160,8 @@ export const ServiceTokenV3Table = ({
|
||||
handlePopUpOpen("serviceTokenV3", {
|
||||
serviceTokenDataId: _id,
|
||||
name,
|
||||
scopes,
|
||||
role,
|
||||
customRole,
|
||||
trustedIps,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ServiceTokenSection } from "./ServiceTokenSection";
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceTokenTab } from "./ServiceTokenTab";
|
||||
@@ -3,12 +3,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
||||
import { WebhooksTab } from "./components/WebhooksTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-project-general" },
|
||||
{ name: "Service Tokens", key: "tab-project-service-tokens" },
|
||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||
];
|
||||
|
||||
@@ -41,9 +39,6 @@ export const ProjectSettingsPage = () => {
|
||||
<Tab.Panel>
|
||||
<ProjectGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ProjectServiceTokensTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<WebhooksTab />
|
||||
</Tab.Panel>
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { ServiceTokenSection } from "../ServiceTokenSection";
|
||||
// import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
|
||||
|
||||
export const ProjectServiceTokensTab = () => {
|
||||
return (
|
||||
<>
|
||||
{/* <ServiceTokenV3Section /> */}
|
||||
<ServiceTokenSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { ProjectServiceTokensTab } from "./ProjectServiceTokensTab";
|
||||
@@ -3,5 +3,4 @@ export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { E2EESection } from "./E2EESection";
|
||||
export { EnvironmentSection } from "./EnvironmentSection";
|
||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
export { ServiceTokenSection } from "./ServiceTokenSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { ContentLoader, Switch, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||
|
||||
enum TabSections {
|
||||
Settings = "settings"
|
||||
}
|
||||
|
||||
export const AdminDashboardPage = () => {
|
||||
const router = useRouter();
|
||||
const data = useServerConfig();
|
||||
const { config } = data;
|
||||
const { user, isLoading: isUserLoading } = useUser();
|
||||
const { orgs } = useOrganization();
|
||||
const { mutate: updateServerConfig } = useUpdateServerConfig();
|
||||
|
||||
const isNotAllowed = !user?.superAdmin;
|
||||
|
||||
useEffect(() => {
|
||||
if (isNotAllowed && !isUserLoading) {
|
||||
if (orgs?.length) {
|
||||
localStorage.setItem("orgData.id", orgs?.[0]?._id);
|
||||
router.push(`/org/${orgs?.[0]?._id}/overview`);
|
||||
}
|
||||
}
|
||||
}, [isNotAllowed, isUserLoading]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl pb-12 text-white dark:[color-scheme:dark]">
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between text-xl">
|
||||
<h1 className="text-3xl font-semibold">Admin Dashboard</h1>
|
||||
<p className="text-base text-bunker-300">Manage your Infisical</p>
|
||||
</div>
|
||||
</div>
|
||||
{isUserLoading || isNotAllowed ? (
|
||||
<ContentLoader text={isNotAllowed ? "Redirecting to org page..." : undefined} />
|
||||
) : (
|
||||
<div>
|
||||
<Tabs defaultValue={TabSections.Settings}>
|
||||
<TabList>
|
||||
<div className="flex flex-row border-b border-mineshaft-600 w-full">
|
||||
<Tab value={TabSections.Settings}>General</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Settings}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Switch
|
||||
id="disable-invite"
|
||||
isChecked={Boolean(config?.allowSignUp)}
|
||||
onCheckedChange={(isChecked) => updateServerConfig({ allowSignUp: isChecked })}
|
||||
/>
|
||||
<div className="flex-grow">Enable signup or invite</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AdminDashboardPage } from "./DashboardPage";
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF";
|
||||
// TODO(akhilmhdh): rewrite this into module functions in lib
|
||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useCreateAdminUser } from "@app/hooks/api";
|
||||
import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto";
|
||||
import { isLoggedIn } from "@app/reactQuery";
|
||||
|
||||
import { DownloadBackupKeys } from "./components/DownloadBackupKeys";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z.string().email().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
password: z.string().trim().min(14).max(100),
|
||||
confirmPassword: z.string().trim()
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Password don't match",
|
||||
path: ["confirmPassword"]
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
enum SignupSteps {
|
||||
DetailsForm = "details-form",
|
||||
BackupKey = "backup-key"
|
||||
}
|
||||
|
||||
export const SignUpPage = () => {
|
||||
const router = useRouter();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
const { createNotification } = useNotificationContext();
|
||||
const [step, setStep] = useState(SignupSteps.DetailsForm);
|
||||
|
||||
const { config } = useServerConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.initialized) {
|
||||
if (isLoggedIn()) {
|
||||
router.push("/admin");
|
||||
} else {
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { mutateAsync: createAdminUser } = useCreateAdminUser();
|
||||
|
||||
const handleFormSubmit = async ({ email, password, firstName, lastName }: TFormSchema) => {
|
||||
// avoid multi submission
|
||||
if (isSubmitting) return;
|
||||
try {
|
||||
const { privateKey, ...userPass } = await generateUserPassKey(email, password);
|
||||
const res = await createAdminUser({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
...userPass
|
||||
});
|
||||
SecurityClient.setToken(res.token);
|
||||
saveTokenToLocalStorage({
|
||||
publicKey: userPass.publicKey,
|
||||
encryptedPrivateKey: userPass.encryptedPrivateKey,
|
||||
iv: userPass.encryptedPrivateKeyIV,
|
||||
tag: userPass.encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
});
|
||||
setStep(SignupSteps.BackupKey);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Faield to create admin"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupKeyGenerate = async () => {
|
||||
try {
|
||||
const { email, password, firstName, lastName } = getValues();
|
||||
const generatedKey = await generateUserBackupKey(email, password);
|
||||
await generateBackupPDFAsync({
|
||||
generatedKey,
|
||||
personalEmail: email,
|
||||
personalName: `${firstName} ${lastName}`
|
||||
});
|
||||
router.push("/admin");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Faield to generate backup"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (config?.initialized && step === SignupSteps.DetailsForm)
|
||||
return <ContentLoader text="Redirecting to admin page..." />;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{step === SignupSteps.DetailsForm && (
|
||||
<motion.div
|
||||
className="text-mineshaft-200"
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="text-center flex flex-col items-center space-y-4">
|
||||
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
<div className="text-4xl">Welcome to Infisical</div>
|
||||
<div>Create your first Admin Account</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="mt-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="firstName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="First name"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input isFullWidth size="lg" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lastName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Last name"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input isFullWidth size="lg" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input isFullWidth size="lg" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input isFullWidth size="lg" type="password" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmPassword"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Confirm password"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input isFullWidth size="lg" type="password" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" isFullWidth className="mt-4" isLoading={isSubmitting}>
|
||||
Let's Go
|
||||
</Button>
|
||||
</form>
|
||||
</motion.div>
|
||||
)}
|
||||
{step === SignupSteps.BackupKey && (
|
||||
<motion.div
|
||||
className="text-mineshaft-200"
|
||||
key="panel-2"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<DownloadBackupKeys onGenerate={handleBackupKeyGenerate} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
onGenerate: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
|
||||
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
|
||||
{t("signup.step4-message")}
|
||||
</p>
|
||||
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
|
||||
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 md:min-w-[23rem] px-3 mx-auto">
|
||||
<span className="mb-2">
|
||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center px-3 justify-center md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading.on();
|
||||
await onGenerate();
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-12"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DownloadBackupKeys } from "./DownloadBackupKeys";
|
||||
@@ -0,0 +1 @@
|
||||
export { SignUpPage } from "./SignUpPage";
|
||||
Reference in New Issue
Block a user