mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
feat: implement openapi (#482)
This commit is contained in:
@@ -67,6 +67,7 @@
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.77.8",
|
||||
"superjson": "2.2.1",
|
||||
"swagger-ui-react": "^5.17.7",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -78,6 +79,7 @@
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.9.1",
|
||||
"node-loader": "^2.0.0",
|
||||
|
||||
@@ -84,6 +84,11 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
icon: IconBrandDocker,
|
||||
href: "/manage/tools/docker",
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.api"),
|
||||
icon: IconPlug,
|
||||
href: "/manage/tools/api",
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.logs"),
|
||||
icon: IconLogs,
|
||||
|
||||
28
apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx
Normal file
28
apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
// workaround for CSS that cannot be processed by next.js, https://github.com/swagger-api/swagger-ui/issues/10045
|
||||
import "./swagger-ui-dark.css";
|
||||
import "./swagger-ui-overrides.css";
|
||||
import "./swagger-ui.css";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
import SwaggerUI from "swagger-ui-react";
|
||||
|
||||
import { openApiDocument } from "@homarr/api";
|
||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
|
||||
return {
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
export default function ApiPage() {
|
||||
const document = openApiDocument(extractBaseUrlFromHeaders(headers()));
|
||||
|
||||
return <SwaggerUI spec={document} />;
|
||||
}
|
||||
1711
apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui-dark.css
Normal file
1711
apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui-dark.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
.swagger-ui .info {
|
||||
margin: 0 !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
9296
apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui.css
Normal file
9296
apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui.css
Normal file
File diff suppressed because it is too large
Load Diff
14
apps/nextjs/src/app/api/[...trpc]/route.ts
Normal file
14
apps/nextjs/src/app/api/[...trpc]/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createOpenApiFetchHandler } from "trpc-swagger/build/index.mjs";
|
||||
|
||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||
|
||||
const handler = (req: Request) => {
|
||||
return createOpenApiFetchHandler({
|
||||
req,
|
||||
endpoint: "/",
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session: null, headers: req.headers }),
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
8
apps/nextjs/src/app/api/openapi/route.ts
Normal file
8
apps/nextjs/src/app/api/openapi/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { openApiDocument } from "@homarr/api";
|
||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||
|
||||
export function GET(request: Request) {
|
||||
return NextResponse.json(openApiDocument(extractBaseUrlFromHeaders(request.headers)));
|
||||
}
|
||||
@@ -43,5 +43,10 @@
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config"
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"trpc-swagger@1.2.6": "patches/trpc-swagger@1.2.6.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"@trpc/server": "next",
|
||||
"dockerode": "^4.0.2",
|
||||
"superjson": "2.2.1",
|
||||
"trpc-swagger": "^1.2.3",
|
||||
"next": "^14.2.6",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
|
||||
import { openApiDocument } from "./open-api";
|
||||
import type { AppRouter } from "./root";
|
||||
import { appRouter } from "./root";
|
||||
import { createCallerFactory, createTRPCContext } from "./trpc";
|
||||
@@ -29,5 +30,5 @@ type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
**/
|
||||
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
|
||||
export { createTRPCContext, appRouter, createCaller };
|
||||
export { createTRPCContext, appRouter, createCaller, openApiDocument };
|
||||
export type { AppRouter, RouterInputs, RouterOutputs };
|
||||
|
||||
11
packages/api/src/open-api.ts
Normal file
11
packages/api/src/open-api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { generateOpenApiDocument } from "trpc-swagger";
|
||||
|
||||
import { appRouter } from "./root";
|
||||
|
||||
export const openApiDocument = (base: string) =>
|
||||
generateOpenApiDocument(appRouter, {
|
||||
title: "Homarr API documentation",
|
||||
version: "1.0.0",
|
||||
baseUrl: base,
|
||||
docsUrl: "https://homarr.dev",
|
||||
});
|
||||
@@ -69,12 +69,16 @@ export const userRouter = createTRPCRouter({
|
||||
// Delete invite as it's used
|
||||
await ctx.db.delete(invites).where(inviteWhere);
|
||||
}),
|
||||
create: publicProcedure.input(validation.user.create).mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
||||
create: publicProcedure
|
||||
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"] } })
|
||||
.input(validation.user.create)
|
||||
.output(z.void())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
||||
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
await createUserAsync(ctx.db, input);
|
||||
}),
|
||||
setProfileImage: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -126,20 +130,33 @@ export const userRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(users.id, input.userId));
|
||||
}),
|
||||
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||
return await ctx.db.query.users.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
provider: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
selectable: publicProcedure.query(async ({ ctx }) => {
|
||||
return await ctx.db.query.users.findMany({
|
||||
getAll: publicProcedure
|
||||
.input(z.void())
|
||||
.output(
|
||||
z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
emailVerified: z.date().nullable(),
|
||||
image: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.meta({ openapi: { method: "GET", path: "/api/users", tags: ["users"] } })
|
||||
.query(({ ctx }) => {
|
||||
return ctx.db.query.users.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
selectable: publicProcedure.query(({ ctx }) => {
|
||||
return ctx.db.query.users.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import type { OpenApiMeta } from "trpc-swagger";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { FlattenError } from "@homarr/common";
|
||||
@@ -46,17 +47,20 @@ export const createTRPCContext = (opts: { headers: Headers; session: Session | n
|
||||
* This is where the trpc api is initialized, connecting the context and
|
||||
* transformer
|
||||
*/
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter: ({ shape, error }) => ({
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
error: error.cause instanceof FlattenError ? error.cause.flatten() : null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const t = initTRPC
|
||||
.context<typeof createTRPCContext>()
|
||||
.meta<OpenApiMeta>()
|
||||
.create({
|
||||
transformer: superjson,
|
||||
errorFormatter: ({ shape, error }) => ({
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
error: error.cause instanceof FlattenError ? error.cause.flatten() : null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a server-side caller
|
||||
|
||||
@@ -1414,6 +1414,7 @@ export default {
|
||||
items: {
|
||||
docker: "Docker",
|
||||
logs: "Logs",
|
||||
api: "API",
|
||||
tasks: "Tasks",
|
||||
},
|
||||
},
|
||||
|
||||
2152
patches/trpc-swagger@1.2.6.patch
Normal file
2152
patches/trpc-swagger@1.2.6.patch
Normal file
File diff suppressed because it is too large
Load Diff
2299
pnpm-lock.yaml
generated
2299
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user