From 2d7efc3ffacb16c27219c26f20fd0682304927e0 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 28 Nov 2024 20:21:29 +0100 Subject: [PATCH] fix: open-api doc generation failing, post patch and delete not exported (#1565) --- apps/nextjs/src/app/api/[...trpc]/route.ts | 13 ++++- packages/api/src/router/user.ts | 2 +- packages/api/src/test/open-api.spec.ts | 16 ++++++ packages/validation/src/user.ts | 57 +++++++++++----------- 4 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 packages/api/src/test/open-api.spec.ts diff --git a/apps/nextjs/src/app/api/[...trpc]/route.ts b/apps/nextjs/src/app/api/[...trpc]/route.ts index 6cf0e899a..ffd59c784 100644 --- a/apps/nextjs/src/app/api/[...trpc]/route.ts +++ b/apps/nextjs/src/app/api/[...trpc]/route.ts @@ -17,6 +17,11 @@ const handlerAsync = async (req: NextRequest) => { const { ua } = userAgent(req); const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua); + // Fallback to JSON if no content type is set + if (!req.headers.has("Content-Type")) { + req.headers.set("Content-Type", "application/json"); + } + return createOpenApiFetchHandler({ req, endpoint: "/", @@ -82,4 +87,10 @@ const getSessionOrDefaultFromHeadersAsync = async ( return await createSessionAsync(db, apiKeyFromDb.user); }; -export { handlerAsync as GET, handlerAsync as POST }; +export { + handlerAsync as GET, + handlerAsync as POST, + handlerAsync as PUT, + handlerAsync as DELETE, + handlerAsync as PATCH, +}; diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 3939343c2..ae3b8cd88 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -503,7 +503,7 @@ export const userRouter = createTRPCRouter({ }), }); -const createUserAsync = async (db: Database, input: z.infer) => { +const createUserAsync = async (db: Database, input: Omit, "groupIds">) => { const salt = await createSaltAsync(); const hashedPassword = await hashPasswordAsync(input.password, salt); diff --git a/packages/api/src/test/open-api.spec.ts b/packages/api/src/test/open-api.spec.ts new file mode 100644 index 000000000..e7d240564 --- /dev/null +++ b/packages/api/src/test/open-api.spec.ts @@ -0,0 +1,16 @@ +import { expect, test, vi } from "vitest"; + +import { openApiDocument } from "../open-api"; + +vi.mock("@homarr/auth", () => ({})); + +test("OpenAPI documentation should be generated", () => { + // Arrange + const base = "https://homarr.dev"; + + // Act + const act = () => openApiDocument(base); + + // Assert + expect(act).not.toThrow(); +}); diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 629f15ec4..f06fb67ec 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -37,43 +37,43 @@ const passwordSchema = z }, ); -const confirmPasswordRefine = [ - (data: { password: string; confirmPassword: string }) => data.password === data.confirmPassword, - { +const addConfirmPasswordRefinement = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: z.ZodObject, +) => { + return schema.refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], params: createCustomErrorParams({ key: "passwordsDoNotMatch", params: {}, }), - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -] satisfies [(args: any) => boolean, unknown]; + }); +}; -const baseCreateUserSchema = z - .object({ - username: usernameSchema, - password: passwordSchema, - confirmPassword: z.string(), - email: z.string().email().or(z.string().length(0).optional()), - }) - .refine(confirmPasswordRefine[0], confirmPasswordRefine[1]); +const baseCreateUserSchema = z.object({ + username: usernameSchema, + password: passwordSchema, + confirmPassword: z.string(), + email: z.string().email().or(z.string().length(0).optional()), + groupIds: z.array(z.string()), +}); -const createUserSchema = baseCreateUserSchema.and(z.object({ groupIds: z.array(z.string()) })); +const createUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema); -const initUserSchema = baseCreateUserSchema; +const initUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema.omit({ groupIds: true })); const signInSchema = z.object({ name: z.string().min(1), password: z.string().min(1), }); -const registrationSchema = z - .object({ +const registrationSchema = addConfirmPasswordRefinement( + z.object({ username: usernameSchema, password: passwordSchema, confirmPassword: z.string(), - }) - .refine(confirmPasswordRefine[0], confirmPasswordRefine[1]); + }), +); const registrationSchemaApi = registrationSchema.and( z.object({ @@ -94,15 +94,16 @@ const editProfileSchema = z.object({ .nullable(), }); -const changePasswordSchema = z - .object({ - previousPassword: z.string().min(1), - password: passwordSchema, - confirmPassword: z.string(), - }) - .refine(confirmPasswordRefine[0], confirmPasswordRefine[1]); +const baseChangePasswordSchema = z.object({ + previousPassword: z.string().min(1), + password: passwordSchema, + confirmPassword: z.string(), + userId: z.string(), +}); -const changePasswordApiSchema = changePasswordSchema.and(z.object({ userId: z.string() })); +const changePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true })); + +const changePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema); const changeHomeBoardSchema = z.object({ homeBoardId: z.string().min(1),