mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
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:
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Button, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { CopyApiKeyModal } from "~/app/[locale]/manage/tools/api/components/copy-api-key-modal";
|
||||
|
||||
interface ApiKeysManagementProps {
|
||||
apiKeys: RouterOutputs["apiKeys"]["getAll"];
|
||||
}
|
||||
|
||||
export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
|
||||
const { openModal } = useModalAction(CopyApiKeyModal);
|
||||
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
|
||||
async onSuccess(data) {
|
||||
openModal({
|
||||
apiKey: data.randomToken,
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/api");
|
||||
},
|
||||
});
|
||||
const t = useScopedI18n("management.page.tool.api.tab.apiKey");
|
||||
|
||||
const columns = useMemo<MRT_ColumnDef<RouterOutputs["apiKeys"]["getAll"][number]>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: t("table.header.id"),
|
||||
},
|
||||
{
|
||||
accessorKey: "user",
|
||||
header: t("table.header.createdBy"),
|
||||
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();
|
||||
}}
|
||||
loading={isPending}
|
||||
>
|
||||
{t("button.createApiToken")}
|
||||
</Button>
|
||||
),
|
||||
enableDensityToggle: false,
|
||||
state: {
|
||||
density: "xs",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title>{t("title")}</Title>
|
||||
<MantineReactTable table={table} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Button, CopyButton, PasswordInput, Stack, Text } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
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");
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import type { OpenAPIV3 } from "openapi-types";
|
||||
import SwaggerUI from "swagger-ui-react";
|
||||
|
||||
// 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";
|
||||
|
||||
interface SwaggerUIClientProps {
|
||||
document: OpenAPIV3.Document;
|
||||
}
|
||||
|
||||
export const SwaggerUIClient = ({ document }: SwaggerUIClientProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const requestInterceptor = (req: Record<string, any>) => {
|
||||
req.credentials = "omit";
|
||||
return req;
|
||||
};
|
||||
|
||||
return <SwaggerUI requestInterceptor={requestInterceptor} spec={document} />;
|
||||
};
|
||||
@@ -1,17 +1,14 @@
|
||||
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 { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
|
||||
|
||||
import { openApiDocument } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { SwaggerUIClient } from "~/app/[locale]/manage/tools/api/components/swagger-ui";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { ApiKeysManagement } from "./components/api-keys";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
@@ -21,8 +18,25 @@ export async function generateMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function ApiPage() {
|
||||
export default async function ApiPage() {
|
||||
const document = openApiDocument(extractBaseUrlFromHeaders(headers()));
|
||||
const apiKeys = await api.apiKeys.getAll();
|
||||
const t = await getScopedI18n("management.page.tool.api.tab");
|
||||
|
||||
return <SwaggerUI spec={document} />;
|
||||
return (
|
||||
<Stack>
|
||||
<Tabs defaultValue={"documentation"}>
|
||||
<TabsList>
|
||||
<TabsTab value={"documentation"}>{t("documentation.label")}</TabsTab>
|
||||
<TabsTab value={"authentication"}>{t("apiKey.label")}</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel value={"authentication"}>
|
||||
<ApiKeysManagement apiKeys={apiKeys} />
|
||||
</TabsPanel>
|
||||
<TabsPanel value={"documentation"}>
|
||||
<SwaggerUIClient document={document} />
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7146,9 +7146,6 @@
|
||||
}
|
||||
.swagger-ui .wrapper {
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
max-width: 1460px;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.swagger-ui .opblock-tag-section {
|
||||
@@ -7734,7 +7731,7 @@
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
|
||||
margin: 0 0 20px;
|
||||
padding: 30px 0;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.swagger-ui .scheme-container .schemes {
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -1,14 +1,59 @@
|
||||
import { createOpenApiFetchHandler } from "trpc-swagger/build/index.mjs";
|
||||
|
||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createSessionAsync } from "@homarr/auth/server";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { apiKeys } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
const handlerAsync = async (req: Request) => {
|
||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
||||
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue);
|
||||
|
||||
const handler = (req: Request) => {
|
||||
return createOpenApiFetchHandler({
|
||||
req,
|
||||
endpoint: "/",
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session: null, headers: req.headers }),
|
||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | null): Promise<Session | null> => {
|
||||
logger.info(
|
||||
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
||||
);
|
||||
|
||||
if (apiKeyHeaderValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
||||
where: eq(apiKeys.apiKey, apiKeyHeaderValue),
|
||||
columns: {
|
||||
id: true,
|
||||
apiKey: false,
|
||||
salt: false,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (apiKeyFromDb === undefined) {
|
||||
logger.warn("An attempt to authenticate over API has failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info(`Read session from API request and found user ${apiKeyFromDb.user.name} (${apiKeyFromDb.user.id})`);
|
||||
return await createSessionAsync(db, apiKeyFromDb.user);
|
||||
};
|
||||
|
||||
export { handlerAsync as GET, handlerAsync as POST };
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
41
packages/api/src/router/apiKeys.ts
Normal file
41
packages/api/src/router/apiKeys.ts
Normal 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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));
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Adapter } from "@auth/core/adapters";
|
||||
import dayjs from "dayjs";
|
||||
import type { NextAuthConfig } from "next-auth";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, users } from "@homarr/db/schema/sqlite";
|
||||
@@ -30,6 +31,21 @@ export const getCurrentUserPermissionsAsync = async (db: Database, userId: strin
|
||||
return getPermissionsWithChildren(permissionKeys);
|
||||
};
|
||||
|
||||
export const createSessionAsync = async (
|
||||
db: Database,
|
||||
user: { id: string; email: string | null },
|
||||
): Promise<Session> => {
|
||||
return {
|
||||
expires: dayjs().add(1, "day").toISOString(),
|
||||
user: {
|
||||
...user,
|
||||
email: user.email ?? "",
|
||||
permissions: await getCurrentUserPermissionsAsync(db, user.id),
|
||||
colorScheme: "auto",
|
||||
},
|
||||
} as Session;
|
||||
};
|
||||
|
||||
export const createSessionCallback = (db: Database): NextAuthCallbackOf<"session"> => {
|
||||
return async ({ session, user }) => {
|
||||
const additionalProperties = await db.query.users.findFirst({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions";
|
||||
export { getIntegrationsWithPermissionsAsync } from "./permissions/integrations-with-permissions";
|
||||
export { isProviderEnabled } from "./providers/check-provider";
|
||||
export { createSessionCallback, createSessionAsync } from "./callbacks";
|
||||
|
||||
9
packages/db/migrations/mysql/0009_wakeful_tenebrous.sql
Normal file
9
packages/db/migrations/mysql/0009_wakeful_tenebrous.sql
Normal file
@@ -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;
|
||||
1481
packages/db/migrations/mysql/meta/0009_snapshot.json
Normal file
1481
packages/db/migrations/mysql/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1727532165317,
|
||||
"tag": "0008_far_lifeguard",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "5",
|
||||
"when": 1728074730696,
|
||||
"tag": "0009_wakeful_tenebrous",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
packages/db/migrations/sqlite/0009_stale_roulette.sql
Normal file
7
packages/db/migrations/sqlite/0009_stale_roulette.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE `apiKey` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`apiKey` text NOT NULL,
|
||||
`salt` text NOT NULL,
|
||||
`userId` text NOT NULL,
|
||||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
1414
packages/db/migrations/sqlite/meta/0009_snapshot.json
Normal file
1414
packages/db/migrations/sqlite/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1727526190343,
|
||||
"tag": "0008_third_thor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1728074724956,
|
||||
"tag": "0009_stale_roulette",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,17 @@ import type {
|
||||
} from "@homarr/definitions";
|
||||
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
||||
|
||||
export const apiKeys = mysqlTable("apiKey", {
|
||||
id: varchar("id", { length: 64 }).notNull().primaryKey(),
|
||||
apiKey: text("apiKey").notNull(),
|
||||
salt: text("salt").notNull(),
|
||||
userId: varchar("userId", { length: 64 })
|
||||
.notNull()
|
||||
.references((): AnyMySqlColumn => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const users = mysqlTable("user", {
|
||||
id: varchar("id", { length: 64 }).notNull().primaryKey(),
|
||||
name: text("name"),
|
||||
@@ -341,6 +352,13 @@ export const serverSettings = mysqlTable("serverSetting", {
|
||||
value: text("value").default('{"json": {}}').notNull(), // empty superjson object
|
||||
});
|
||||
|
||||
export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [apiKeys.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const searchEngines = mysqlTable("search_engine", {
|
||||
id: varchar("id", { length: 64 }).notNull().primaryKey(),
|
||||
iconUrl: text("icon_url").notNull(),
|
||||
|
||||
@@ -20,6 +20,17 @@ import type {
|
||||
WidgetKind,
|
||||
} from "@homarr/definitions";
|
||||
|
||||
export const apiKeys = sqliteTable("apiKey", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
apiKey: text("apiKey").notNull(),
|
||||
salt: text("salt").notNull(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references((): AnySQLiteColumn => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const users = sqliteTable("user", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name"),
|
||||
@@ -343,6 +354,13 @@ export const serverSettings = sqliteTable("serverSetting", {
|
||||
value: text("value").default('{"json": {}}').notNull(), // empty superjson object
|
||||
});
|
||||
|
||||
export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [apiKeys.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const searchEngines = sqliteTable("search_engine", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
iconUrl: text("icon_url").notNull(),
|
||||
|
||||
@@ -1887,6 +1887,35 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
api: {
|
||||
title: "API",
|
||||
modal: {
|
||||
createApiToken: {
|
||||
title: "API token created",
|
||||
description:
|
||||
"API token was created. Be careful, this token is encrypted in the database and will never be transferred again to you. If you loose this token, you'll no longer be able to retrieve this specific token.",
|
||||
button: "Copy and close",
|
||||
},
|
||||
},
|
||||
tab: {
|
||||
documentation: {
|
||||
label: "Documentation",
|
||||
},
|
||||
apiKey: {
|
||||
label: "Authentication",
|
||||
title: "API Keys",
|
||||
button: {
|
||||
createApiToken: "Create API token",
|
||||
},
|
||||
table: {
|
||||
header: {
|
||||
id: "ID",
|
||||
createdBy: "Created by",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
about: {
|
||||
version: "Version {version}",
|
||||
|
||||
Reference in New Issue
Block a user