feat: rebuild onboarding experience

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:32:11 +02:00
1 parent d091362db2
commit e41605640b
249 files changed
+8216 -14060

No files matched your search

+11 -2
View File
@@ -8,6 +8,11 @@ import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import {
getOnboardingClaimTokenFromCookieHeader,
isClaimOnlyOnboardingAccessAllowedAsync,
} from "@homarr/api/onboarding-claim";
import { normalizeOnboardingStep } from "@homarr/api/onboarding-step";
import { auth } from "@homarr/auth/next";
import { env } from "@homarr/common/env";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
@@ -75,13 +80,17 @@ const reEncryptSecrets = (tempDb: InstanceType<typeof Database>, importedKeyHex:
const isOnboardingActiveAsync = async (): Promise<boolean> => {
const onboardingRow = await db.query.onboarding.findFirst();
if (!onboardingRow) return false;
return onboardingRow.step === "start";
return normalizeOnboardingStep(onboardingRow.step) === "start";
};
export async function POST(req: Request) {
const session = await auth();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const isOnboarding = !isAdmin && (await isOnboardingActiveAsync());
const hasOnboardingClaim = await isClaimOnlyOnboardingAccessAllowedAsync(
db,
getOnboardingClaimTokenFromCookieHeader(req.headers.get("cookie")),
);
const isOnboarding = !isAdmin && hasOnboardingClaim && (await isOnboardingActiveAsync());
if (!isAdmin && !isOnboarding) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import {
claimOnboardingAsync,
getOnboardingClaimTokenFromCookieHeader,
onboardingClaimCookieName,
} from "@homarr/api/onboarding-claim";
import { auth } from "@homarr/auth/next";
import { db } from "@homarr/db";
const noStoreHeaders = { "Cache-Control": "no-store" };
export async function POST(request: Request) {
const fetchSite = request.headers.get("sec-fetch-site");
if (fetchSite && fetchSite !== "same-origin" && fetchSite !== "none") {
return NextResponse.json({ error: "Cross-site onboarding claims are not allowed." }, { status: 403 });
}
const session = await auth();
const result = await claimOnboardingAsync(db, {
currentToken: getOnboardingClaimTokenFromCookieHeader(request.headers.get("cookie")),
force: session?.user.permissions.includes("admin") ?? false,
});
if (result.status === "finished") {
const response = NextResponse.json({ error: "Onboarding is already complete." }, { status: 409 });
response.cookies.delete(onboardingClaimCookieName);
return response;
}
if (result.status === "forbidden") {
return NextResponse.json({ error: "Administrator access is required." }, { status: 403 });
}
if (result.status === "locked") {
return NextResponse.json(
{ error: "Another browser has already claimed onboarding.", expiresAt: result.expiresAt },
{ status: 423, headers: noStoreHeaders },
);
}
if (result.status !== "issued" && result.status !== "active") {
return NextResponse.json({ error: "Onboarding could not be claimed." }, { status: 409 });
}
const forwardedProtocol = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
const secure = forwardedProtocol === "https" || new URL(request.url).protocol === "https:";
const response = NextResponse.json(
{ status: result.status, expiresAt: result.expiresAt },
{ headers: noStoreHeaders },
);
response.cookies.set(onboardingClaimCookieName, result.token, {
httpOnly: true,
sameSite: "strict",
secure,
path: "/",
expires: new Date(result.expiresAt),
});
return response;
}