feat: rebuild onboarding experience
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user