feat: add api keys (#991)

* feat: add api keys

* chore: address pull request feedback

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2024-10-05 16:18:31 +02:00
committed by GitHub
parent ee8375756c
commit b14f82b4bb
22 changed files with 3374 additions and 60 deletions

View File

@@ -8,4 +8,12 @@ export const openApiDocument = (base: string) =>
version: "1.0.0",
baseUrl: base,
docsUrl: "https://homarr.dev",
securitySchemes: {
apikey: {
type: "apiKey",
name: "ApiKey",
description: "API key which can be obtained in the Homarr administration dashboard",
in: "header",
},
},
});

View File

@@ -1,3 +1,4 @@
import { apiKeysRouter } from "./router/apiKeys";
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { cronJobsRouter } from "./router/cron-jobs";
@@ -31,6 +32,7 @@ export const appRouter = createTRPCRouter({
docker: dockerRouter,
serverSettings: serverSettingsRouter,
cronJobs: cronJobsRouter,
apiKeys: apiKeysRouter,
});
// export type definition of API

View File

@@ -0,0 +1,41 @@
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { generateSecureRandomToken } from "@homarr/common/server";
import { createId, db } from "@homarr/db";
import { apiKeys } from "@homarr/db/schema/sqlite";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
export const apiKeysRouter = createTRPCRouter({
getAll: permissionRequiredProcedure.requiresPermission("admin").query(() => {
return db.query.apiKeys.findMany({
columns: {
id: true,
apiKey: false,
salt: false,
},
with: {
user: {
columns: {
id: true,
name: true,
image: true,
},
},
},
});
}),
create: permissionRequiredProcedure.requiresPermission("admin").mutation(async ({ ctx }) => {
const salt = await createSaltAsync();
const randomToken = generateSecureRandomToken(64);
const hashedRandomToken = await hashPasswordAsync(randomToken, salt);
await db.insert(apiKeys).values({
id: createId(),
apiKey: hashedRandomToken,
salt,
userId: ctx.session.user.id,
});
return {
randomToken,
};
}),
});

View File

@@ -7,53 +7,114 @@ import { validation, z } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../trpc";
export const appRouter = createTRPCRouter({
all: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.apps.findMany({
orderBy: asc(apps.name),
});
}),
selectable: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.apps.findMany({
columns: {
id: true,
name: true,
iconUrl: true,
},
orderBy: asc(apps.name),
});
}),
all: publicProcedure
.input(z.void())
.output(
z.array(
z.object({
name: z.string(),
id: z.string(),
description: z.string().nullable(),
iconUrl: z.string(),
href: z.string().nullable(),
}),
),
)
.meta({ openapi: { method: "GET", path: "/api/apps", tags: ["apps"], protect: true } })
.query(({ ctx }) => {
return ctx.db.query.apps.findMany({
orderBy: asc(apps.name),
});
}),
search: publicProcedure
.input(z.object({ query: z.string(), limit: z.number().min(1).max(100).default(10) }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.apps.findMany({
.output(
z.array(
z.object({
name: z.string(),
id: z.string(),
description: z.string().nullable(),
iconUrl: z.string(),
href: z.string().nullable(),
}),
),
)
.meta({ openapi: { method: "GET", path: "/api/apps/search", tags: ["apps"], protect: true } })
.query(({ ctx, input }) => {
return ctx.db.query.apps.findMany({
where: like(apps.name, `%${input.query}%`),
orderBy: asc(apps.name),
limit: input.limit,
});
}),
byId: publicProcedure.input(validation.common.byId).query(async ({ ctx, input }) => {
const app = await ctx.db.query.apps.findFirst({
where: eq(apps.id, input.id),
});
if (!app) {
throw new TRPCError({
code: "NOT_FOUND",
message: "App not found",
selectable: publicProcedure
.input(z.void())
.output(
z.array(
z.object({
name: z.string(),
id: z.string(),
iconUrl: z.string(),
}),
),
)
.meta({
openapi: {
method: "GET",
path: "/api/apps/selectable",
tags: ["apps"],
protect: true,
},
})
.query(({ ctx }) => {
return ctx.db.query.apps.findMany({
columns: {
id: true,
name: true,
iconUrl: true,
},
orderBy: asc(apps.name),
});
}),
byId: publicProcedure
.input(validation.common.byId)
.output(
z.object({
name: z.string(),
id: z.string(),
description: z.string().nullable(),
iconUrl: z.string(),
href: z.string().nullable(),
}),
)
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.query(async ({ ctx, input }) => {
const app = await ctx.db.query.apps.findFirst({
where: eq(apps.id, input.id),
});
}
return app;
}),
create: publicProcedure.input(validation.app.manage).mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values({
id: createId(),
name: input.name,
description: input.description,
iconUrl: input.iconUrl,
href: input.href,
});
}),
if (!app) {
throw new TRPCError({
code: "NOT_FOUND",
message: "App not found",
});
}
return app;
}),
create: publicProcedure
.input(validation.app.manage)
.output(z.void())
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values({
id: createId(),
name: input.name,
description: input.description,
iconUrl: input.iconUrl,
href: input.href,
});
}),
update: publicProcedure.input(validation.app.edit).mutation(async ({ ctx, input }) => {
const app = await ctx.db.query.apps.findFirst({
where: eq(apps.id, input.id),
@@ -76,7 +137,11 @@ export const appRouter = createTRPCRouter({
})
.where(eq(apps.id, input.id));
}),
delete: publicProcedure.input(validation.common.byId).mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id));
}),
delete: publicProcedure
.output(z.void())
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.input(validation.common.byId)
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id));
}),
});

View File

@@ -70,7 +70,7 @@ export const userRouter = createTRPCRouter({
await ctx.db.delete(invites).where(inviteWhere);
}),
create: publicProcedure
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"] } })
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
.input(validation.user.create)
.output(z.void())
.mutation(async ({ ctx, input }) => {
@@ -143,7 +143,7 @@ export const userRouter = createTRPCRouter({
}),
),
)
.meta({ openapi: { method: "GET", path: "/api/users", tags: ["users"] } })
.meta({ openapi: { method: "GET", path: "/api/users", tags: ["users"], protect: true } })
.query(({ ctx }) => {
return ctx.db.query.users.findMany({
columns: {