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

@@ -0,0 +1,56 @@
import { describe, expect, test } from "vitest";
import { onboardingCreateIntegrationSchema, onboardingDiscoveredAppSchema } from "./onboarding";
describe("onboarding URL validation", () => {
test.each(["javascript:alert(1)", "data:text/html,hello", "file:///etc/passwd"])(
"rejects the %s scheme for integrations",
(url) => {
expect(onboardingCreateIntegrationSchema.safeParse({ name: "Unsafe", url }).success).toBe(false);
},
);
test("accepts HTTP services and icon identifiers", () => {
expect(
onboardingDiscoveredAppSchema.safeParse({
name: "Sonarr",
sourceId: "docker:sonarr",
href: "http://sonarr:8989",
pingUrl: "https://sonarr.example.com/ping",
iconUrl: "sonarr",
description: "TV automation",
}).success,
).toBe(true);
});
test("accepts bounded integration metadata and rejects invalid metadata", () => {
expect(
onboardingCreateIntegrationSchema.safeParse({
name: "Sonarr",
url: "http://sonarr:8989",
pingUrl: "https://sonarr.example.com/ping",
iconUrl: "https://icons.example/sonarr.svg",
description: "TV automation",
}).success,
).toBe(true);
expect(
onboardingCreateIntegrationSchema.safeParse({
name: "Sonarr",
url: "http://sonarr:8989",
pingUrl: "file:///etc/passwd",
description: "x".repeat(513),
}).success,
).toBe(false);
});
test("rejects non-web app and ping URLs", () => {
expect(
onboardingDiscoveredAppSchema.safeParse({
name: "Unsafe",
href: "javascript:alert(1)",
pingUrl: "file:///etc/passwd",
iconUrl: null,
}).success,
).toBe(false);
});
});
+62
View File
@@ -0,0 +1,62 @@
import { z } from "zod/v4";
import { colorSchemes, onboardingLayoutPresets, widgetKinds } from "@homarr/definitions";
import { supportedLanguages } from "@homarr/translation/languages";
import { boardNameSchema } from "./board";
const hexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
const httpUrlSchema = z.url().refine((value) => {
const protocol = new URL(value).protocol;
return protocol === "http:" || protocol === "https:";
}, "Only HTTP and HTTPS URLs are permitted");
const uniqueIdsSchema = z
.array(z.string().trim().min(1).max(255))
.max(128)
.refine((ids) => new Set(ids).size === ids.length, { message: "IDs must be unique" });
export const onboardingCompleteSetupSchema = z.object({
server: z.object({
defaultLocale: z.enum(supportedLanguages),
defaultColorScheme: z.enum(colorSchemes),
analyticsEnabled: z.boolean().optional(),
}),
board: z.object({
id: z.string().trim().min(1).max(255).optional(),
name: boardNameSchema,
primaryColor: hexColorSchema,
secondaryColor: hexColorSchema,
itemRadius: z.enum(["xs", "sm", "md", "lg", "xl"]),
layoutPreset: z.enum(onboardingLayoutPresets).default("balanced"),
leftSidebar: z.boolean().default(false),
rightSidebar: z.boolean().default(false),
}),
selectedIntegrationIds: uniqueIdsSchema.default([]),
selectedAppIds: uniqueIdsSchema.default([]),
selectedDockerSourceIds: uniqueIdsSchema.default([]),
selectedWidgetKinds: z
.array(z.enum(widgetKinds))
.max(widgetKinds.length)
.refine((kinds) => new Set(kinds).size === kinds.length, { message: "Widget kinds must be unique" })
.default([]),
});
export const onboardingCreateIntegrationSchema = z.object({
name: z.string().trim().min(1).max(127),
url: httpUrlSchema,
sourceId: z.string().trim().min(1).max(512).optional(),
iconUrl: z.string().trim().min(1).max(2048).nullable().optional(),
description: z.string().trim().max(512).nullable().optional(),
pingUrl: httpUrlSchema.nullable().optional(),
});
export const onboardingDiscoveredAppSchema = z.object({
sourceId: z.string().trim().min(1).max(512).optional(),
name: z.string().trim().min(1).max(127),
href: httpUrlSchema.nullable(),
pingUrl: httpUrlSchema.nullable().optional(),
iconUrl: z.string().trim().min(1).nullable(),
description: z.string().trim().max(512).nullable().optional(),
});
export type OnboardingCompleteSetupInput = z.infer<typeof onboardingCompleteSetupSchema>;
-13
View File
@@ -1,13 +0,0 @@
import { z } from "zod/v4";
export const settingsInitSchema = z.object({
analytics: z.object({
enableGeneral: z.boolean(),
}),
crawlingAndIndexing: z.object({
noIndex: z.boolean(),
noFollow: z.boolean(),
noTranslate: z.boolean(),
noSiteLinksSearchBox: z.boolean(),
}),
});