diff --git a/backend/src/plugins/core/admin/install/layout/layout.service.ts b/backend/src/plugins/core/admin/install/layout/layout.service.ts index ba698169c..6a1eea599 100644 --- a/backend/src/plugins/core/admin/install/layout/layout.service.ts +++ b/backend/src/plugins/core/admin/install/layout/layout.service.ts @@ -8,8 +8,6 @@ import { } from "./dto/layout.obj"; import { core_users } from "@/plugins/core/admin/database/schema/users"; -import { core_sessions } from "@/plugins/core/admin/database/schema/sessions"; -import { core_admin_sessions } from "@/plugins/core/admin/database/schema/admins"; import { core_languages } from "@/plugins/core/admin/database/schema/languages"; import { DatabaseService } from "@/database/database.service"; @@ -22,22 +20,7 @@ export class LayoutAdminInstallService { .select({ count: count() }) .from(core_users); if (users[0].count > 0) { - const [sessionCount, sessionCountAdmin] = [ - await this.databaseService.db - .select({ count: count() }) - .from(core_sessions), - await this.databaseService.db - .select({ count: count() }) - .from(core_admin_sessions) - ]; - - if (sessionCount[0].count > 0 || sessionCountAdmin[0].count > 0) { - throw new AccessDeniedError(); - } - - return { - status: LayoutAdminInstallEnum.FINISH - }; + throw new AccessDeniedError(); } const languages = await this.databaseService.db diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/account/page.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/account/page.tsx deleted file mode 100644 index b2f660369..000000000 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/account/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AccountInstallConfigsView } from "@/plugins/admin/configs/views/install/steps/account/account-install-configs-view"; - -export default function Page() { - return ; -} diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/database/page.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/database/page.tsx deleted file mode 100644 index 44fbfbdb6..000000000 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/database/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { DatabaseInstallConfigsView } from "@/plugins/admin/configs/views/install/steps/database/database-install-configs-view"; - -export default function Page() { - return ; -} diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/error.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/error.tsx deleted file mode 100644 index f657b0ad8..000000000 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/error.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -import { InternalErrorView } from "@/plugins/admin/global/internal-error/internal-error-view"; - -export default function Error() { - return ; -} diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/layout.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/layout.tsx deleted file mode 100644 index b4622c4f2..000000000 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from "react"; -import { redirect } from "@vitnode/frontend/navigation"; - -import { - Admin__Install__Layout, - Admin__Install__LayoutQuery, - Admin__Install__LayoutQueryVariables -} from "@/graphql/hooks"; -import { fetcher, ErrorType } from "@/graphql/fetcher"; -import { LayoutInstallConfigsView } from "@/plugins/admin/configs/views/install/layout-install-configs-view"; -import { InternalErrorView } from "@/plugins/admin/global/internal-error/internal-error-view"; -import { RedirectsInstallConfigsLayout } from "@/plugins/admin/configs/views/install/redirects-install-configs-layout"; - -interface Props { - children: React.ReactNode; -} - -const getData = async () => { - const { data } = await fetcher< - Admin__Install__LayoutQuery, - Admin__Install__LayoutQueryVariables - >({ - query: Admin__Install__Layout, - cache: "force-cache" - }); - - return data; -}; - -export default async function Layout({ children }: Props) { - try { - const data = await getData(); - - return ( - - {children} - - ); - } catch (error) { - const code = error as ErrorType; - - if (code.extensions?.code === "ACCESS_DENIED") { - redirect("/admin"); - } - - return ; - } -} diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/license/page.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/license/page.tsx deleted file mode 100644 index 611ae4f64..000000000 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/license/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { LicenseInstallConfigsView } from "@/plugins/admin/configs/views/install/steps/license/license-install-configs-view"; - -export default function Page() { - return ; -} diff --git a/frontend/app/[locale]/(admin)/admin/(configs)/install/page.tsx b/frontend/app/[locale]/(admin)/admin/(configs)/install/page.tsx index b8a632f4f..e044a9c49 100644 --- a/frontend/app/[locale]/(admin)/admin/(configs)/install/page.tsx +++ b/frontend/app/[locale]/(admin)/admin/(configs)/install/page.tsx @@ -1,5 +1,40 @@ -import { InstallConfigsView } from "@/plugins/admin/configs/views/install/steps/install-configs-view"; +import { redirect } from "@vitnode/frontend/navigation"; -export default function Page() { - return ; +import { ErrorType, fetcher } from "@/graphql/fetcher"; +import { + Admin__Install__Layout, + Admin__Install__LayoutQuery, + Admin__Install__LayoutQueryVariables +} from "@/graphql/hooks"; +import { LayoutInstallConfigsView } from "@/plugins/admin/configs/views/install/layout-install-configs-view"; +import { InternalErrorView } from "@/plugins/admin/global/internal-error/internal-error-view"; + +const getData = async () => { + const { data } = await fetcher< + Admin__Install__LayoutQuery, + Admin__Install__LayoutQueryVariables + >({ + query: Admin__Install__Layout, + cache: "force-cache" + }); + + return data; +}; + +export default async function Page() { + try { + const data = await getData(); + + return ( + + ); + } catch (error) { + const code = error as ErrorType; + + if (code.extensions?.code === "ACCESS_DENIED") { + redirect("/admin"); + } + + return ; + } } diff --git a/frontend/components/steps/steps.tsx b/frontend/components/steps/steps.tsx index 8e470e107..1a3b5b496 100644 --- a/frontend/components/steps/steps.tsx +++ b/frontend/components/steps/steps.tsx @@ -3,6 +3,7 @@ import { Link } from "@vitnode/frontend/navigation"; import { cn } from "@vitnode/frontend/helpers"; export interface ItemStepProps { + component: React.ReactNode; id: string; title: string; checked?: boolean; diff --git a/frontend/plugins/admin/configs/views/install/finish/finish-install-config-view.tsx b/frontend/plugins/admin/configs/views/install/finish/finish-install-config-view.tsx index 6a1603bb4..db575165b 100644 --- a/frontend/plugins/admin/configs/views/install/finish/finish-install-config-view.tsx +++ b/frontend/plugins/admin/configs/views/install/finish/finish-install-config-view.tsx @@ -1,4 +1,3 @@ -import { useTranslations } from "next-intl"; import { Home, KeyRound } from "lucide-react"; import { Link } from "@vitnode/frontend/navigation"; @@ -13,28 +12,28 @@ import { import { buttonVariants } from "@/components/ui/button"; export const FinishInstallConfigsView = () => { - const t = useTranslations("admin.configs.install.finish"); - return ( - {t("title", { name: "VitNode" })} - {t("desc")} + Welcome to VitNode! + Your website is ready. - {t("text")} + + The installation process is complete. You can now use your website. + - {t("buttons.home_page")} + Home Page - {t("buttons.admin_control_panel")} + Admin Control Panel diff --git a/frontend/plugins/admin/configs/views/install/hooks/use-install-vitnode.ts b/frontend/plugins/admin/configs/views/install/hooks/use-install-vitnode.ts new file mode 100644 index 000000000..6825125ea --- /dev/null +++ b/frontend/plugins/admin/configs/views/install/hooks/use-install-vitnode.ts @@ -0,0 +1,13 @@ +import * as React from "react"; + +interface Args { + currentStep: number; + setCurrentStep: React.Dispatch>; +} + +export const InstallVitNodeContext = React.createContext({ + currentStep: 0, + setCurrentStep: () => {} +}); + +export const useInstallVitnode = () => React.useContext(InstallVitNodeContext); diff --git a/frontend/plugins/admin/configs/views/install/layout-install-configs-view.tsx b/frontend/plugins/admin/configs/views/install/layout-install-configs-view.tsx index c87b7aad6..783b502ac 100644 --- a/frontend/plugins/admin/configs/views/install/layout-install-configs-view.tsx +++ b/frontend/plugins/admin/configs/views/install/layout-install-configs-view.tsx @@ -1,8 +1,6 @@ "use client"; import * as React from "react"; -import { useTranslations } from "next-intl"; -import { useSelectedLayoutSegment } from "next/navigation"; import { Steps, ItemStepProps } from "@/components/steps/steps"; import { @@ -11,64 +9,84 @@ import { CardHeader, CardTitle } from "@/components/ui/card"; +import { InstallConfigsView } from "./steps/install-configs-view"; +import { InstallVitNodeContext } from "./hooks/use-install-vitnode"; +import { LicenseInstallConfigsView } from "./steps/license/license-install-configs-view"; +import { DatabaseInstallConfigsView } from "./steps/database/database-install-configs-view"; +import { AccountInstallConfigsView } from "./steps/account/account-install-configs-view"; +import { LayoutAdminInstallEnum } from "@/graphql/hooks"; +import { FinishInstallConfigsView } from "./finish/finish-install-config-view"; interface Props { - children: React.ReactNode; + data: LayoutAdminInstallEnum; } -export const LayoutInstallConfigsView = ({ children }: Props) => { - const t = useTranslations("admin.configs.install"); - const segment = useSelectedLayoutSegment(); - - const stepsNumber: Record = { - license: 2, - database: 3, - account: 4 - }; - - const activeStep = segment ? stepsNumber[segment] : 1; +export const LayoutInstallConfigsView = ({ data }: Props) => { + const [currentStep, setCurrentStep] = React.useState(0); const items: ItemStepProps[] = [ { id: "welcome", - title: t("steps.welcome.title"), - description: t("steps.welcome.desc"), - checked: activeStep >= 2 + title: "Welcome", + description: "Before you begin...", + checked: currentStep >= 1, + component: }, { id: "license", - title: t("steps.license.title"), - description: t("steps.license.desc"), - checked: activeStep >= 3 + title: "License", + description: "Read carefully", + checked: currentStep >= 2, + component: }, { id: "database", - title: t("steps.database.title"), - description: t("steps.database.desc"), - checked: activeStep >= 4 + title: "Database", + description: "Create schema and first records", + checked: currentStep >= 3, + component: }, { id: "account", - title: t("steps.account.title"), - description: t("steps.account.desc"), - checked: activeStep >= 5 + title: "Admin Account", + description: "Create admin account", + checked: currentStep >= 4, + component: } ]; + React.useEffect(() => { + if (currentStep < 2) { + return; + } + + if (data === LayoutAdminInstallEnum.account && currentStep < 3) { + setCurrentStep(3); + } + }, [data, currentStep]); + + if (currentStep === items.length) { + return ; + } + return ( - {t("title", { name: "VitNode" })} - - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-expect-error */} - {t(`steps.${items.at(activeStep - 1)?.id}.title`)} - + Install VitNode + {items.at(currentStep)?.title} - {children} + + + {items.at(currentStep)?.component} + ); diff --git a/frontend/plugins/admin/configs/views/install/redirects-install-configs-layout.tsx b/frontend/plugins/admin/configs/views/install/redirects-install-configs-layout.tsx deleted file mode 100644 index 1f43544e5..000000000 --- a/frontend/plugins/admin/configs/views/install/redirects-install-configs-layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import * as React from "react"; -import { usePathname, useRouter } from "@vitnode/frontend/navigation"; - -import { - LayoutAdminInstallEnum, - Admin__Install__LayoutQuery -} from "@/graphql/hooks"; -import { FinishInstallConfigsView } from "./finish/finish-install-config-view"; - -interface Props { - children: React.ReactNode; - data: Admin__Install__LayoutQuery; -} - -export const RedirectsInstallConfigsLayout = async ({ - children, - data -}: Props) => { - const { replace } = useRouter(); - const pathname = usePathname(); - - React.useEffect(() => { - const redirectItems: Record = { - [LayoutAdminInstallEnum.database]: "/admin/install", - [LayoutAdminInstallEnum.account]: "/admin/install/account" - }; - - const path = redirectItems[data.admin__install__layout.status]; - - if (path && path !== pathname) { - replace(path); - } - }, [data]); - - if (data.admin__install__layout.status === LayoutAdminInstallEnum.finish) { - return ; - } - - return <>{children}>; -}; diff --git a/frontend/plugins/admin/configs/views/install/steps/account/account-install-configs-view.tsx b/frontend/plugins/admin/configs/views/install/steps/account/account-install-configs-view.tsx index 9aa78d4e9..d2425f6d1 100644 --- a/frontend/plugins/admin/configs/views/install/steps/account/account-install-configs-view.tsx +++ b/frontend/plugins/admin/configs/views/install/steps/account/account-install-configs-view.tsx @@ -19,13 +19,22 @@ import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Progress } from "@/components/ui/progress"; +import { useInstallVitnode } from "../../hooks/use-install-vitnode"; + export const AccountInstallConfigsView = () => { const t = useTranslations("core"); const { form, onSubmit } = useSignUpView(); + const { setCurrentStep } = useInstallVitnode(); return ( - + { + await form.handleSubmit(onSubmit)(val); + setCurrentStep(prev => prev + 1); + }} + > { {(fieldState.invalid || value.length > 0) && ( - Weak - Strong + {t("week")} + {t("strong")} { - const t = useTranslations("admin.configs.install.steps.database"); - return ( <> - {t("text")} + Now VitNode needs to create default records in your database. diff --git a/frontend/plugins/admin/configs/views/install/steps/database/mutation-api.ts b/frontend/plugins/admin/configs/views/install/steps/database/mutation-api.ts index a9f518725..645b4bee2 100644 --- a/frontend/plugins/admin/configs/views/install/steps/database/mutation-api.ts +++ b/frontend/plugins/admin/configs/views/install/steps/database/mutation-api.ts @@ -18,7 +18,7 @@ export const mutationApi = async () => { query: Admin__Install__Create_Database }); - revalidatePath("/admin/(configs)/install/", "layout"); + revalidatePath("/admin/install", "page"); return { data }; } catch (error) { diff --git a/frontend/plugins/admin/configs/views/install/steps/database/submit-database-install-configs.tsx b/frontend/plugins/admin/configs/views/install/steps/database/submit-database-install-configs.tsx index a4a301e0b..4fe68d1ae 100644 --- a/frontend/plugins/admin/configs/views/install/steps/database/submit-database-install-configs.tsx +++ b/frontend/plugins/admin/configs/views/install/steps/database/submit-database-install-configs.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useTranslations } from "next-intl"; import * as React from "react"; import { toast } from "sonner"; @@ -7,10 +5,12 @@ import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { mutationApi } from "./mutation-api"; +import { useInstallVitnode } from "../../hooks/use-install-vitnode"; + export const SubmitDatabaseInstallConfigs = () => { const [isPending, setPending] = React.useState(false); - const t = useTranslations("admin.configs.install.steps.database"); const tCore = useTranslations("core"); + const { setCurrentStep } = useInstallVitnode(); return ( { toast.error(tCore("errors.title"), { description: tCore("errors.internal_server_error") }); + setPending(false); + + return; } setPending(false); + setCurrentStep(prev => prev + 1); }} loading={isPending} > - {t("submit")} + Create Records ); }; diff --git a/frontend/plugins/admin/configs/views/install/steps/install-configs-view.tsx b/frontend/plugins/admin/configs/views/install/steps/install-configs-view.tsx index 3021a0554..12a8e48f9 100644 --- a/frontend/plugins/admin/configs/views/install/steps/install-configs-view.tsx +++ b/frontend/plugins/admin/configs/views/install/steps/install-configs-view.tsx @@ -1,22 +1,21 @@ -import { useTranslations } from "next-intl"; -import { Link } from "@vitnode/frontend/navigation"; - import { CardContent, CardFooter } from "@/components/ui/card"; -import { buttonVariants } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; +import { useInstallVitnode } from "../hooks/use-install-vitnode"; export const InstallConfigsView = () => { - const t = useTranslations("admin.configs.install"); + const { setCurrentStep } = useInstallVitnode(); return ( <> - {t("steps.welcome.text", { name: "VitNode" })} + Welcome to the installation wizard for VitNode. This wizard will guide + you through the installation process. - - {t("steps.next_step")} - + setCurrentStep(prev => prev + 1)}> + Next step + > ); diff --git a/frontend/plugins/admin/configs/views/install/steps/license/form-license-install-configs.tsx b/frontend/plugins/admin/configs/views/install/steps/license/form-license-install-configs.tsx index 3eaa70cc0..39bb5e9d8 100644 --- a/frontend/plugins/admin/configs/views/install/steps/license/form-license-install-configs.tsx +++ b/frontend/plugins/admin/configs/views/install/steps/license/form-license-install-configs.tsx @@ -1,10 +1,7 @@ -"use client"; - import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { useRouter } from "@vitnode/frontend/navigation"; import { Form, @@ -16,10 +13,11 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; +import { useInstallVitnode } from "../../hooks/use-install-vitnode"; + export const FormLicenseInstallConfigs = () => { - const t = useTranslations("admin.configs.install.steps"); const tCore = useTranslations("core"); - const { push } = useRouter(); + const { setCurrentStep } = useInstallVitnode(); const formSchema = z.object({ agree: z.boolean({ @@ -35,7 +33,7 @@ export const FormLicenseInstallConfigs = () => { }); const onSubmit = () => { - push("/admin/install/database"); + setCurrentStep(prev => prev + 1); }; return ( @@ -55,13 +53,15 @@ export const FormLicenseInstallConfigs = () => { onCheckedChange={field.onChange} /> - {t("license.agree")} + + I agree to the terms of the license agreement. + )} /> - {t("next_step")} + Next step diff --git a/frontend/plugins/admin/configs/views/install/steps/license/license-install-configs-view.tsx b/frontend/plugins/admin/configs/views/install/steps/license/license-install-configs-view.tsx index 0b233db8a..88228884e 100644 --- a/frontend/plugins/admin/configs/views/install/steps/license/license-install-configs-view.tsx +++ b/frontend/plugins/admin/configs/views/install/steps/license/license-install-configs-view.tsx @@ -1,12 +1,9 @@ -import { useTranslations } from "next-intl"; import { Link } from "@vitnode/frontend/navigation"; import { CardContent, CardFooter } from "@/components/ui/card"; import { FormLicenseInstallConfigs } from "./form-license-install-configs"; export const LicenseInstallConfigsView = () => { - const t = useTranslations("admin.configs.install.steps"); - return ( <> @@ -15,7 +12,7 @@ export const LicenseInstallConfigsView = () => { target="_blank" className="text-primary underline" > - {t("license.link")} + Click here to read the license agreement. diff --git a/frontend/plugins/admin/langs/en.json b/frontend/plugins/admin/langs/en.json index 2408f4825..fc340e848 100644 --- a/frontend/plugins/admin/langs/en.json +++ b/frontend/plugins/admin/langs/en.json @@ -500,43 +500,7 @@ } }, "configs": { - "mobile_not_supported": "This view is not supported on mobile devices.", - "install": { - "title": "Install {name}", - "steps": { - "next_step": "Next step", - "welcome": { - "title": "Welcome", - "desc": "Before you begin...", - "text": "Welcome to the installation wizard for {name}. This wizard will guide you through the installation process." - }, - "license": { - "title": "License", - "desc": "Read carefully", - "link": "Click here to read the license agreement.", - "agree": "I agree to the terms of the license agreement." - }, - "database": { - "title": "Database", - "text": "Now VitNode needs to create default records in your database.", - "desc": "Create schema and first records", - "submit": "Create Records" - }, - "account": { - "title": "Admin Account", - "desc": "Create admin account" - } - }, - "finish": { - "title": "Welcome to {name}!", - "desc": "Your website is ready.", - "text": "The installation process is complete. You can now use your website.", - "buttons": { - "home_page": "Home Page", - "admin_control_panel": "Admin Control Panel" - } - } - } + "mobile_not_supported": "This view is not supported on mobile devices." } } } diff --git a/frontend/plugins/admin/langs/pl.json b/frontend/plugins/admin/langs/pl.json index a9e2e4e5c..05d0e0d2b 100644 --- a/frontend/plugins/admin/langs/pl.json +++ b/frontend/plugins/admin/langs/pl.json @@ -501,43 +501,7 @@ } }, "configs": { - "mobile_not_supported": "Ten widok nie jest obsługiwany na urządzeniach mobilnych.", - "install": { - "title": "Instalacja {name}", - "steps": { - "next_step": "Następny krok", - "welcome": { - "title": "Witaj", - "desc": "Zanim zaczniesz...", - "text": "Witaj w kreatorze instalacji {name}. Ten kreator przeprowadzi Cię przez proces instalacji." - }, - "license": { - "title": "Licencja", - "desc": "Przeczytaj uważnie", - "link": "Kliknij tutaj, aby przeczytać licencję.", - "agree": "Akceptuje warunki licencji" - }, - "database": { - "title": "Baza danych", - "text": "Teraz VitNode potrzebuje stworzyć domyślne rekordy w Twojej bazie danych.", - "desc": "Stwórz schemat oraz pierwsze rekordy", - "submit": "Stwórz rekordy" - }, - "account": { - "title": "Konto admina", - "desc": "Stwórz konto administratora" - } - }, - "finish": { - "title": "Witaj w {name}!", - "desc": "Twoja strona jest gotowa.", - "text": "Proces instalacji jest gotowy. Możesz teraz użyć swojej strony internetowej.", - "buttons": { - "home_page": "Strona główna", - "admin_control_panel": "Panel kontrolny administratora" - } - } - } + "mobile_not_supported": "Ten widok nie jest obsługiwany na urządzeniach mobilnych." } } } diff --git a/frontend/plugins/core/langs/en.json b/frontend/plugins/core/langs/en.json index 860e0ef5c..f4fb9fbea 100644 --- a/frontend/plugins/core/langs/en.json +++ b/frontend/plugins/core/langs/en.json @@ -1,5 +1,7 @@ { "core": { + "week": "Week", + "strong": "Strong", "reload_page": "Reload Page", "link_url_with_link": "Link URL: ", "yes": "Yes", diff --git a/frontend/plugins/core/langs/pl.json b/frontend/plugins/core/langs/pl.json index 4ee466246..6453da79f 100644 --- a/frontend/plugins/core/langs/pl.json +++ b/frontend/plugins/core/langs/pl.json @@ -1,5 +1,7 @@ { "core": { + "week": "Słabe", + "strong": "Silne", "reload_page": "Przeładuj stronę", "link_url_with_link": "Link URL: ", "yes": "Tak", diff --git a/frontend/themes/1/core/views/auth/sign/up/form/form-sign-up.tsx b/frontend/themes/1/core/views/auth/sign/up/form/form-sign-up.tsx index 5c99c9949..9ea28ec7d 100644 --- a/frontend/themes/1/core/views/auth/sign/up/form/form-sign-up.tsx +++ b/frontend/themes/1/core/views/auth/sign/up/form/form-sign-up.tsx @@ -100,8 +100,8 @@ export const FormSignUp = () => { {(fieldState.invalid || value.length > 0) && ( - Weak - Strong + {t("week")} + {t("strong")}
{t("text")}
+ The installation process is complete. You can now use your website. +
Now VitNode needs to create default records in your database.
{t("steps.welcome.text", { name: "VitNode" })}