diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 9900176e1..8b0487292 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -336,5 +336,9 @@ "step5-invite-team": "Invite your team", "step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.", "step5-skip": "Skip" + }, + "admin": { + "signup-title": "Admin Sign Up", + "dashboard": "Admin Dashboard" } } diff --git a/frontend/src/components/utilities/checks/OnboardingCheck.ts b/frontend/src/components/utilities/checks/OnboardingCheck.ts index 01d7a2d55..b3abc9b85 100644 --- a/frontend/src/components/utilities/checks/OnboardingCheck.ts +++ b/frontend/src/components/utilities/checks/OnboardingCheck.ts @@ -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; } diff --git a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx index f43ee923c..c4759ae01 100644 --- a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx +++ b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx @@ -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 ( -
+
loading animation {text && isTextArray && ( diff --git a/frontend/src/components/v2/Switch/Switch.tsx b/frontend/src/components/v2/Switch/Switch.tsx index c66da2204..a54657fc6 100644 --- a/frontend/src/components/v2/Switch/Switch.tsx +++ b/frontend/src/components/v2/Switch/Switch.tsx @@ -3,7 +3,7 @@ import * as SwitchPrimitive from "@radix-ui/react-switch"; import { twMerge } from "tailwind-merge"; export type SwitchProps = Omit & { - children: ReactNode; + children?: ReactNode; id: string; isChecked?: boolean; isRequired?: boolean; diff --git a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx index fb1a21d40..1c1d732e1 100644 --- a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx +++ b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx @@ -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 ( - +

{text}

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"} -

- ) -} \ No newline at end of file + ); +}; diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 267d80ccd..67340780d 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -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 - }, -] \ No newline at end of file + } +]; diff --git a/frontend/src/context/AuthContext/AuthContext.tsx b/frontend/src/context/AuthContext/AuthContext.tsx index 7515e459c..3c615485b 100644 --- a/frontend/src/context/AuthContext/AuthContext.tsx +++ b/frontend/src/context/AuthContext/AuthContext.tsx @@ -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 (
- infisical loading indicator + infisical loading indicator
); } diff --git a/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx b/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx new file mode 100644 index 000000000..fba1600cc --- /dev/null +++ b/frontend/src/context/ServerConfigContext/ServerConfigContext.tsx @@ -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(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(() => { + 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 ( +
+ +
+ ); + } + + return {children}; +}; + +export const useServerConfig = () => { + const ctx = useContext(ServerConfigContext); + if (!ctx) { + throw new Error("useServerConfig has to be used within "); + } + + return ctx; +}; diff --git a/frontend/src/context/ServerConfigContext/index.tsx b/frontend/src/context/ServerConfigContext/index.tsx new file mode 100644 index 000000000..aa7e7a4d0 --- /dev/null +++ b/frontend/src/context/ServerConfigContext/index.tsx @@ -0,0 +1 @@ +export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext"; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 35c39f533..11c729080 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -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"; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts new file mode 100644 index 000000000..e1c4301d4 --- /dev/null +++ b/frontend/src/hooks/api/admin/index.ts @@ -0,0 +1,2 @@ +export { useCreateAdminUser, useUpdateServerConfig } from "./mutation"; +export { useGetServerConfig } from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts new file mode 100644 index 000000000..ea53ff1db --- /dev/null +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -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>({ + 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()); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts new file mode 100644 index 000000000..f64c8bfa9 --- /dev/null +++ b/frontend/src/hooks/api/admin/queries.ts @@ -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 + >, + "queryKey" | "queryFn" + >; +} = {}) => + useQuery({ + queryKey: adminQueryKeys.serverConfig(), + queryFn: fetchServerConfig, + ...options, + enabled: options?.enabled ?? true + }); diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts new file mode 100644 index 000000000..72453ad44 --- /dev/null +++ b/frontend/src/hooks/api/admin/types.ts @@ -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; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index ea5bb658d..c5e719218 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,4 +1,5 @@ export * from "./apiKeys"; +export * from "./admin" export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index c6b790592..3b57e8953 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -14,6 +14,7 @@ export type User = { createdAt: Date; updatedAt: Date; email: string; + superAdmin: boolean; firstName?: string; lastName?: string; authProvider?: AuthMethod; diff --git a/frontend/src/layouts/AdminLayout/AdminLayout.tsx b/frontend/src/layouts/AdminLayout/AdminLayout.tsx new file mode 100644 index 000000000..ad156d748 --- /dev/null +++ b/frontend/src/layouts/AdminLayout/AdminLayout.tsx @@ -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 = [ + [ + , + "Support Forum", + "https://infisical.com/slack" + ], + [ + , + "Read Docs", + "https://infisical.com/docs/documentation/getting-started/introduction" + ], + [ + , + "GitHub Issues", + "https://github.com/Infisical/infisical/issues" + ], + [ + , + "Email Support", + "mailto:support@infisical.com" + ] +]; + +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 ( + <> +
+
+ +
+ {children} +
+
+
+
+ +

+ {` ${t("common.no-mobile")} `} +

+
+ + ); +}; diff --git a/frontend/src/layouts/AdminLayout/index.tsx b/frontend/src/layouts/AdminLayout/index.tsx new file mode 100644 index 000000000..1a136d62f --- /dev/null +++ b/frontend/src/layouts/AdminLayout/index.tsx @@ -0,0 +1 @@ +export { AdminLayout } from "./AdminLayout"; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 0757196c9..5b19b657c 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -399,6 +399,13 @@ export const AppLayout = ({ children }: LayoutProps) => { /> + {user?.superAdmin && ( + + + Admin Panel + + + )}
{!isLoading && loginError && } - {!serverDetails?.inviteOnlySignup ? ( + {config.allowSignUp ? (
@@ -236,7 +236,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: ) : (
)} -
+
Forgot password? Recover your account diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx new file mode 100644 index 000000000..6099a4d91 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -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 ( +
+
+
+

Admin Dashboard

+

Manage your Infisical.

+
+
+ {isUserLoading || isNotAllowed ? ( + + ) : ( +
+ + +
+ General +
+
+ +
+ updateServerConfig({ allowSignUp: isChecked })} + /> +
Enable signup or invite
+
+
+
+
+ )} +
+ ); +}; diff --git a/frontend/src/views/admin/DashboardPage/index.tsx b/frontend/src/views/admin/DashboardPage/index.tsx new file mode 100644 index 000000000..1d5f062e6 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/index.tsx @@ -0,0 +1 @@ +export { AdminDashboardPage } from "./DashboardPage"; diff --git a/frontend/src/views/admin/SignUpPage/SignUpPage.tsx b/frontend/src/views/admin/SignUpPage/SignUpPage.tsx new file mode 100644 index 000000000..544204b13 --- /dev/null +++ b/frontend/src/views/admin/SignUpPage/SignUpPage.tsx @@ -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; + +export const SignUpPage = () => { + const router = useRouter(); + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + 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 ; + + return ( +
+
+
+ Infisical logo +
Welcome to Infisical
+
Create your first Admin Account
+
+
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+
+
+ ); +}; diff --git a/frontend/src/views/admin/SignUpPage/index.tsx b/frontend/src/views/admin/SignUpPage/index.tsx new file mode 100644 index 000000000..aed0b2d78 --- /dev/null +++ b/frontend/src/views/admin/SignUpPage/index.tsx @@ -0,0 +1 @@ +export { SignUpPage } from "./SignUpPage";