Skip to content

Commit

Permalink
feat: add api keys
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Sep 6, 2024
1 parent 8c20553 commit 6ea0a20
Show file tree
Hide file tree
Showing 16 changed files with 3,414 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import { Button, Stack, Title, Group, Text } from "@mantine/core";
import type { MRT_ColumnDef} from "mantine-react-table";
import {MantineReactTable, useMantineReactTable } from "mantine-react-table";
import {useMemo} from "react";
import type {RouterOutputs} from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useModalAction } from "@homarr/modals";
import { CopyApiKeyModal } from "~/app/[locale]/manage/tools/api/components/copy-api-key-modal";
import { UserAvatar } from "@homarr/ui";

interface ApiKeysManagementProps {
apiKeys: RouterOutputs["apiKeys"]["getAll"]
}

export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
const { openModal } = useModalAction(CopyApiKeyModal);
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
onSettled: (data) => {
openModal({
apiKey: data.randomToken
});
}
});

const columns = useMemo<MRT_ColumnDef<RouterOutputs["apiKeys"]["getAll"][number]>[]>(
() => [
{
accessorKey: 'id', //access nested data with dot notation
header: 'ID',
},
{
accessorKey: 'apikey',
header: 'Key',
},
{
accessorKey: 'user',
header: 'Created by',
Cell: ({ row }) => (
<Group gap={"xs"}>
<UserAvatar user={row.original.user} size={"sm"} />
<Text>{row.original.user.name}</Text>
</Group>
)
}
],
[],
);

const table = useMantineReactTable({
columns,
data: apiKeys,
renderTopToolbarCustomActions: () => (
<Button
onClick={() => {
mutate();
}}
disabled={isPending}
loading={isPending}
>
Create API token
</Button>
),
enableDensityToggle: false,
state: {
density: "xs"
}
});

return <Stack>
<Title>API Keys</Title>
<MantineReactTable table={table} />
</Stack>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createModal } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { Stack, Text, Button, CopyButton, PasswordInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";

export const CopyApiKeyModal = createModal<{ apiKey: string }>(({ actions, innerProps }) => {
const t = useScopedI18n("management.page.tool.api.modal.createApiToken");
const [visible, { toggle }] = useDisclosure(false);
return (
<Stack>
<Text>{t("description")}</Text>
<PasswordInput
value={innerProps.apiKey}
visible={visible}
onVisibilityChange={toggle}
readOnly
/>
<CopyButton value={innerProps.apiKey}>
{({ copy }) => (
<Button
onClick={() => {
copy();
actions.closeModal();
}}
variant="default"
fullWidth
>
{t("button")}
</Button>
)}
</CopyButton>
</Stack>
);
}).withOptions({
defaultTitle(t) {
return t("management.page.tool.api.modal.createApiToken.title");
},
});
24 changes: 20 additions & 4 deletions apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import "./swagger-ui.css";

import { headers } from "next/headers";
import SwaggerUI from "swagger-ui-react";
import { ApiKeysManagement } from "./components/api-keys";

import { openApiDocument } from "@homarr/api";
import { extractBaseUrlFromHeaders } from "@homarr/common";

import { createMetaTitle } from "~/metadata";
import { api } from "@homarr/api/server";
import { Stack, Tabs, TabsList, TabsTab, TabsPanel } from "@mantine/core";

export async function generateMetadata() {
const t = await getScopedI18n("management");
Expand All @@ -21,8 +23,22 @@ export async function generateMetadata() {
};
}

export default function ApiPage() {
export default async function ApiPage() {
const document = openApiDocument(extractBaseUrlFromHeaders(headers()));

return <SwaggerUI spec={document} />;
const apiKeys = await api.apiKeys.getAll();

return <Stack>
<Tabs defaultValue={"documentation"}>
<TabsList>
<TabsTab value={"documentation"}>Documentation</TabsTab>
<TabsTab value={"authentication"}>Authentication</TabsTab>
</TabsList>
<TabsPanel value={"authentication"}>
<ApiKeysManagement apiKeys={apiKeys}/>
</TabsPanel>
<TabsPanel value={"documentation"}>
<SwaggerUI spec={document}/>
</TabsPanel>
</Tabs>
</Stack>;
}
5 changes: 1 addition & 4 deletions apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { serverSettingsRouter } from "./router/serverSettings";
import { userRouter } from "./router/user";
import { widgetRouter } from "./router/widgets";
import { createTRPCRouter } from "./trpc";
import {apiKeysRouter} from "./router/apiKeys";

export const appRouter = createTRPCRouter({
user: userRouter,
Expand All @@ -29,6 +30,7 @@ export const appRouter = createTRPCRouter({
docker: dockerRouter,
serverSettings: serverSettingsRouter,
cronJobs: cronJobsRouter,
apiKeys: apiKeysRouter,
});

// export type definition of API
Expand Down
44 changes: 44 additions & 0 deletions packages/api/src/router/apiKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {createTRPCRouter, permissionRequiredProcedure} from "../trpc";
import {createId, db} from "@homarr/db";
import { apiKeys } from "@homarr/db/schema/sqlite";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { generateSecureRandomToken } from "@homarr/common/server";

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: salt,
userId: ctx.session.user.id
});
return {
randomToken,
};
})
});
9 changes: 9 additions & 0 deletions packages/db/migrations/mysql/0008_small_scream.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `apiKey` (
`id` varchar(64) NOT NULL,
`apiKey` text NOT NULL,
`salt` text NOT NULL,
`userId` varchar(64) NOT NULL,
CONSTRAINT `apiKey_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `apiKey` ADD CONSTRAINT `apiKey_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;
Loading

0 comments on commit 6ea0a20

Please sign in to comment.