fix(onboarding): return to welcome before administrator creation (#7012)

This commit is contained in:
Thomas authored and GitHub committed 2026-10-03 22:34:05 +02:00
1 parent b40f000dd5
commit 83ae98847a
3 files changed
+55 -6

No files matched your search

@@ -16,7 +16,9 @@ import {
} from "@tabler/icons-react";
After Homarr starts, open its URL. An unfinished installation redirects to the onboarding screen at `/init`. Choose
**Get started** for a new installation or **Restore backup** to import a compatible SQLite backup.
**Get started** for a new installation or **Restore backup** to import a compatible SQLite backup. Before creating the
administrator, you can return to Welcome with **Back**, including after refreshing `/init`. Creating the administrator
commits this setup stage; later steps do not rewind it.
<Callout type="info" title="Persist /appdata before you continue">
Onboarding writes the administrator and board configuration to the database. If onboarding returns after a restart,
@@ -8,7 +8,7 @@ import { createId } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { handleTransactionsAsync, eq, inArray, like, or } from "@homarr/db";
import { handleTransactionsAsync, and, eq, inArray, like, notExists, or } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import {
apps,
@@ -25,6 +25,7 @@ import {
sectionLayouts,
sections,
serverSettings,
users,
} from "@homarr/db/schema";
import {
emptySuperJSON,
@@ -147,6 +148,23 @@ export const onboardRouter = createTRPCRouter({
await nextOnboardingStepAsync(ctx.db);
}),
returnToWelcome: onboardingProcedure.requiresStep("user").mutation(async ({ ctx }) => {
const returnedRows = await ctx.db
.update(onboarding)
.set({ step: "start", previousStep: null })
.where(
and(
eq(onboarding.step, "user"),
eq(onboarding.previousStep, "start"),
notExists(ctx.db.select({ id: users.id }).from(users)),
),
)
.returning({ id: onboarding.id });
if (returnedRows.length !== 1) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Administrator setup can no longer be restarted." });
}
}),
testIntegration: onboardingProcedure
.requiresStep("setup")
.input(onboardingIntegrationDraftSchema)
+33 -4
View File
@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from "react";
import { Alert, Button, Stack, Text, TextInput } from "@mantine/core";
import { IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react";
import { IconArrowLeft, IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
@@ -36,11 +36,12 @@ const AccountShell = ({
);
export const AccountSetup = ({ environment }: OnboardingStudioProps) => {
if (environment.currentStep === "user") return <CredentialsSetup />;
if (environment.currentStep === "user") return <CredentialsSetup canReturnToWelcome={!environment.hasUsers} />;
return <ExternalGroupSetup />;
};
const CredentialsSetup = () => {
const CredentialsSetup = ({ canReturnToWelcome }: { canReturnToWelcome: boolean }) => {
const navigationT = useI18n("init.studio");
const t = useI18n("init.studio.account");
const [requiresSignIn, setRequiresSignIn] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
@@ -54,6 +55,16 @@ const CredentialsSetup = () => {
showErrorNotification({ title: t("errorTitle"), message });
},
});
const returnToWelcome = clientApi.onboard.returnToWelcome.useMutation({
async onSuccess() {
sounds.swoosh();
await revalidatePathActionAsync("/init");
},
onError() {
sounds.error();
showErrorNotification({ title: t("errorTitle"), message: t("unknownError") });
},
});
const form = useZodForm(userInitSchema, {
initialValues: { username: "", password: "", confirmPassword: "" },
});
@@ -119,9 +130,27 @@ const CredentialsSetup = () => {
{submitError}
</Alert>
) : null}
<Button type="submit" size="md" loading={form.submitting} rightSection={<IconArrowRight size={18} />}>
<Button
type="submit"
size="md"
loading={form.submitting}
disabled={returnToWelcome.isPending}
rightSection={<IconArrowRight size={18} />}
>
{t("create")}
</Button>
{canReturnToWelcome && (
<Button
type="button"
variant="default"
leftSection={<IconArrowLeft size={18} />}
loading={returnToWelcome.isPending}
disabled={form.submitting}
onClick={() => returnToWelcome.mutate()}
>
{navigationT("back")}
</Button>
)}
</Stack>
</form>
</AccountShell>