mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
feat(onboarding): frontend for onboarding users
This commit is contained in:
@@ -336,5 +336,9 @@
|
|||||||
"step5-invite-team": "Invite your team",
|
"step5-invite-team": "Invite your team",
|
||||||
"step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.",
|
"step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.",
|
||||||
"step5-skip": "Skip"
|
"step5-skip": "Skip"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"signup-title": "Admin Sign Up",
|
||||||
|
"dashboard": "Admin Dashboard"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fetchOrgUsers,fetchUserAction } from "@app/hooks/api/users/queries";
|
import { fetchOrgUsers, fetchUserAction } from "@app/hooks/api/users/queries";
|
||||||
|
|
||||||
interface OnboardingCheckProps {
|
interface OnboardingCheckProps {
|
||||||
|
orgId: string;
|
||||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||||
setHasUserClickedSlack?: (value: boolean) => void;
|
setHasUserClickedSlack?: (value: boolean) => void;
|
||||||
setHasUserClickedIntro?: (value: boolean) => void;
|
setHasUserClickedIntro?: (value: boolean) => void;
|
||||||
@@ -12,6 +13,7 @@ interface OnboardingCheckProps {
|
|||||||
* This function checks which onboarding steps a user has already finished.
|
* This function checks which onboarding steps a user has already finished.
|
||||||
*/
|
*/
|
||||||
const onboardingCheck = async ({
|
const onboardingCheck = async ({
|
||||||
|
orgId,
|
||||||
setTotalOnboardingActionsDone,
|
setTotalOnboardingActionsDone,
|
||||||
setHasUserClickedSlack,
|
setHasUserClickedSlack,
|
||||||
setHasUserClickedIntro,
|
setHasUserClickedIntro,
|
||||||
@@ -19,9 +21,7 @@ const onboardingCheck = async ({
|
|||||||
setUsersInOrg
|
setUsersInOrg
|
||||||
}: OnboardingCheckProps) => {
|
}: OnboardingCheckProps) => {
|
||||||
let countActions = 0;
|
let countActions = 0;
|
||||||
const userActionSlack = await fetchUserAction(
|
const userActionSlack = await fetchUserAction("slack_cta_clicked");
|
||||||
"slack_cta_clicked"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userActionSlack) {
|
if (userActionSlack) {
|
||||||
countActions += 1;
|
countActions += 1;
|
||||||
@@ -41,9 +41,8 @@ const onboardingCheck = async ({
|
|||||||
}
|
}
|
||||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||||
|
|
||||||
const orgId = localStorage.getItem("orgData.id");
|
|
||||||
const orgUsers = await fetchOrgUsers(orgId || "");
|
const orgUsers = await fetchOrgUsers(orgId || "");
|
||||||
|
|
||||||
if (orgUsers.length > 1) {
|
if (orgUsers.length > 1) {
|
||||||
countActions += 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
text?: string | string[];
|
text?: string | string[];
|
||||||
frequency?: number;
|
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 [pos, setPos] = useState(0);
|
||||||
const isTextArray = Array.isArray(text);
|
const isTextArray = Array.isArray(text);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -23,7 +25,12 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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" />
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
{text && isTextArray && (
|
{text && isTextArray && (
|
||||||
<AnimatePresence exitBeforeEnter>
|
<AnimatePresence exitBeforeEnter>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disabled" | "required"> & {
|
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disabled" | "required"> & {
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
id: string;
|
id: string;
|
||||||
isChecked?: boolean;
|
isChecked?: boolean;
|
||||||
isRequired?: boolean;
|
isRequired?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||||
useGetOrgTrialUrl
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
@@ -16,38 +14,36 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||||
const link = (subscription && subscription.slug !== null)
|
const link =
|
||||||
? `/org/${currentOrg?._id}/billing`
|
subscription && subscription.slug !== null
|
||||||
: "https://infisical.com/scheduledemo";
|
? `/org/${currentOrg?._id}/billing`
|
||||||
|
: "https://infisical.com/scheduledemo";
|
||||||
|
|
||||||
const handleUpgradeBtnClick = async () => {
|
const handleUpgradeBtnClick = async () => {
|
||||||
try {
|
try {
|
||||||
if (!subscription || !currentOrg) return;
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
if (!subscription.has_used_trial) {
|
if (!subscription.has_used_trial) {
|
||||||
// direct user to start pro trial
|
// direct user to start pro trial
|
||||||
|
|
||||||
const url = await mutateAsync({
|
const url = await mutateAsync({
|
||||||
orgId: currentOrg._id,
|
orgId: currentOrg._id,
|
||||||
success_url: window.location.href
|
success_url: window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
} else {
|
} else {
|
||||||
// direct user to upgrade their plan
|
// direct user to upgrade their plan
|
||||||
window.location.href = link;
|
window.location.href = link;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent title="Unleash Infisical's Full Power">
|
||||||
title="Unleash Infisical's Full Power"
|
|
||||||
>
|
|
||||||
<p className="mb-2 text-bunker-300">{text}</p>
|
<p className="mb-2 text-bunker-300">{text}</p>
|
||||||
<p className="text-bunker-300">
|
<p className="text-bunker-300">
|
||||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
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}
|
onClick={handleUpgradeBtnClick}
|
||||||
className="mr-4"
|
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>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => onOpenChange && onOpenChange(false)}
|
onClick={() => onOpenChange && onOpenChange(false)}
|
||||||
>
|
>
|
||||||
@@ -71,5 +67,5 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
|||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export const publicPaths = [
|
|||||||
"/saml-sso",
|
"/saml-sso",
|
||||||
"/login/provider/success", // TODO: change
|
"/login/provider/success", // TODO: change
|
||||||
"/login/provider/error", // TODO: change
|
"/login/provider/error", // TODO: change
|
||||||
"/login/sso"
|
"/login/sso",
|
||||||
|
"/admin/signup"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const languageMap = {
|
export const languageMap = {
|
||||||
@@ -50,7 +51,8 @@ const plansProd: Mapping = {
|
|||||||
|
|
||||||
export const plans = plansProd || plansDev;
|
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 = [
|
export const secretTagsColors = [
|
||||||
{
|
{
|
||||||
@@ -115,5 +117,5 @@ export const secretTagsColors = [
|
|||||||
rgba: "rgb(255,0,0, 0.8)",
|
rgba: "rgb(255,0,0, 0.8)",
|
||||||
name: "Red",
|
name: "Red",
|
||||||
selected: false
|
selected: false
|
||||||
},
|
}
|
||||||
]
|
];
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Props = {
|
|||||||
// Provide a context for whole app to notify user is authorized or not
|
// Provide a context for whole app to notify user is authorized or not
|
||||||
export const AuthProvider = ({ children }: Props): JSX.Element => {
|
export const AuthProvider = ({ children }: Props): JSX.Element => {
|
||||||
const { isLoading } = useGetAuthToken();
|
const { isLoading } = useGetAuthToken();
|
||||||
const { pathname, push } = useRouter();
|
const { pathname, push, asPath } = useRouter();
|
||||||
const [isReady, setIsReady] = useToggle(false);
|
const [isReady, setIsReady] = useToggle(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,7 +26,7 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
|
|||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
// not a public path and not authenticated kick to login page
|
// not a public path and not authenticated kick to login page
|
||||||
if (!publicPaths.includes(pathname) && !isLoggedIn()) {
|
if (!publicPaths.includes(pathname) && !isLoggedIn()) {
|
||||||
push("/login").then(() => {
|
push({ pathname: "/login", query: { redirect: asPath } }).then(() => {
|
||||||
setIsReady.on();
|
setIsReady.on();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -40,7 +40,12 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
|
|||||||
if (isLoading || !isReady) {
|
if (isLoading || !isReady) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
|
<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>
|
</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,
|
ProjectPermissionSub,
|
||||||
useProjectPermission
|
useProjectPermission
|
||||||
} from "./ProjectPermissionContext";
|
} from "./ProjectPermissionContext";
|
||||||
|
export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext";
|
||||||
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
|
||||||
export { UserProvider, useUser } from "./UserContext";
|
export { UserProvider, useUser } from "./UserContext";
|
||||||
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
|
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;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./apiKeys";
|
export * from "./apiKeys";
|
||||||
|
export * from "./admin"
|
||||||
export * from "./auditLogs";
|
export * from "./auditLogs";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type User = {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
email: string;
|
email: string;
|
||||||
|
superAdmin: boolean;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
authProvider?: AuthMethod;
|
authProvider?: AuthMethod;
|
||||||
|
|||||||
@@ -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>
|
</DropdownMenuItem>
|
||||||
</a>
|
</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" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<button type="button" onClick={logOutUser} className="w-full">
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
@@ -541,7 +548,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
{/* <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export { AdminLayout } from "./AdminLayout";
|
||||||
export { AppLayout } from "./AppLayout";
|
export { AppLayout } from "./AppLayout";
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
type TUserPassKey = {
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
publicKey: string;
|
||||||
|
verifier: string;
|
||||||
|
salt: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateUserPassKey = async (email: string, password: string) => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
|
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||||
|
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||||
|
return new Promise<TUserPassKey>((resolve, reject) => {
|
||||||
|
client.init({ username: email, password }, () => {
|
||||||
|
client.createVerifier(
|
||||||
|
async (err: any, { salt, verifier }: { salt: string; verifier: string }) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: moduralize into KeyService
|
||||||
|
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 resolve({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyTag,
|
||||||
|
protectedKeyIV,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
publicKey,
|
||||||
|
verifier,
|
||||||
|
salt,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
+29
-22
@@ -20,6 +20,7 @@ import {
|
|||||||
OrgPermissionProvider,
|
OrgPermissionProvider,
|
||||||
OrgProvider,
|
OrgProvider,
|
||||||
ProjectPermissionProvider,
|
ProjectPermissionProvider,
|
||||||
|
ServerConfigProvider,
|
||||||
SubscriptionProvider,
|
SubscriptionProvider,
|
||||||
UserProvider,
|
UserProvider,
|
||||||
WorkspaceProvider
|
WorkspaceProvider
|
||||||
@@ -84,36 +85,42 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<AuthProvider>
|
<ServerConfigProvider>
|
||||||
<Component {...pageProps} />
|
<AuthProvider>
|
||||||
</AuthProvider>
|
<Component {...pageProps} />
|
||||||
|
</AuthProvider>
|
||||||
|
</ServerConfigProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Layout = Component?.layout || AppLayout;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<AuthProvider>
|
<NotificationProvider>
|
||||||
<OrgProvider>
|
<ServerConfigProvider>
|
||||||
<OrgPermissionProvider>
|
<AuthProvider>
|
||||||
<WorkspaceProvider>
|
<OrgProvider>
|
||||||
<ProjectPermissionProvider>
|
<OrgPermissionProvider>
|
||||||
<SubscriptionProvider>
|
<WorkspaceProvider>
|
||||||
<UserProvider>
|
<ProjectPermissionProvider>
|
||||||
<NotificationProvider>
|
<SubscriptionProvider>
|
||||||
<AppLayout>
|
<UserProvider>
|
||||||
<Component {...pageProps} />
|
<Layout>
|
||||||
</AppLayout>
|
<Component {...pageProps} />
|
||||||
</NotificationProvider>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</SubscriptionProvider>
|
</SubscriptionProvider>
|
||||||
</ProjectPermissionProvider>
|
</ProjectPermissionProvider>
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
</OrgPermissionProvider>
|
</OrgPermissionProvider>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ServerConfigProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</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(() => {
|
useEffect(() => {
|
||||||
onboardingCheck({
|
onboardingCheck({
|
||||||
|
orgId: currentOrg,
|
||||||
setHasUserClickedIntro,
|
setHasUserClickedIntro,
|
||||||
setHasUserClickedSlack,
|
setHasUserClickedSlack,
|
||||||
setHasUserPushedSecrets,
|
setHasUserPushedSecrets,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import InitialSignupStep from "@app/components/signup/InitialSignupStep";
|
|||||||
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
||||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import { useServerConfig } from "@app/context";
|
||||||
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
|
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
@@ -34,6 +35,13 @@ export default function SignUp() {
|
|||||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutateAsync } = useVerifyEmailVerificationCode();
|
const { mutateAsync } = useVerifyEmailVerificationCode();
|
||||||
|
const { config } = useServerConfig();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config.allowSignUp) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}, [config.allowSignUp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tryAuth = async () => {
|
const tryAuth = async () => {
|
||||||
@@ -65,7 +73,7 @@ export default function SignUp() {
|
|||||||
const { token } = await mutateAsync({ email, code });
|
const { token } = await mutateAsync({ email, code });
|
||||||
SecurityClient.setSignupToken(token);
|
SecurityClient.setSignupToken(token);
|
||||||
setStep(3);
|
setStep(3);
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setCodeError(true);
|
setCodeError(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useServerConfig } from "@app/context";
|
||||||
|
|
||||||
import { navigateUserToOrg } from "../../Login.utils";
|
import { navigateUserToOrg } from "../../Login.utils";
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loginError, setLoginError] = useState(false);
|
const [loginError, setLoginError] = useState(false);
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { config } = useServerConfig();
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -84,7 +84,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigateUserToOrg(router);
|
await navigateUserToOrg(router);
|
||||||
|
|
||||||
// case: login does not require MFA step
|
// case: login does not require MFA step
|
||||||
@@ -225,7 +225,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||||
{!serverDetails?.inviteOnlySignup ? (
|
{config.allowSignUp ? (
|
||||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
<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 />
|
||||||
)}
|
)}
|
||||||
<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">
|
<Link href="/verify-email">
|
||||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
<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
|
Forgot password? Recover your account
|
||||||
|
|||||||
@@ -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="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 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,164 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
// 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 { generateUserPassKey } from "@app/lib/crypto";
|
||||||
|
import { isLoggedIn } from "@app/reactQuery";
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
export const SignUpPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const { config } = useServerConfig();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config?.initialized) {
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
router.push("/admin");
|
||||||
|
} else {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [config.initialized]);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Faield to create admin"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config?.initialized) return <ContentLoader text="Redirecting to admin page..." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<div className="text-mineshaft-200">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SignUpPage } from "./SignUpPage";
|
||||||
Reference in New Issue
Block a user