diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx new file mode 100644 index 00000000..a82a9b35 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx @@ -0,0 +1,468 @@ +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogDescription, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + prefix: z.string().optional(), + expiresIn: z.number().nullable(), + organizationId: z.string().min(1, "Organization is required"), + // Rate limiting fields + rateLimitEnabled: z.boolean().optional(), + rateLimitTimeWindow: z.number().nullable(), + rateLimitMax: z.number().nullable(), + // Request limiting fields + remaining: z.number().nullable().optional(), + refillAmount: z.number().nullable().optional(), + refillInterval: z.number().nullable().optional(), +}); + +type FormValues = z.infer; + +const EXPIRATION_OPTIONS = [ + { label: "Never", value: "0" }, + { label: "1 day", value: String(60 * 60 * 24) }, + { label: "7 days", value: String(60 * 60 * 24 * 7) }, + { label: "30 days", value: String(60 * 60 * 24 * 30) }, + { label: "90 days", value: String(60 * 60 * 24 * 90) }, + { label: "1 year", value: String(60 * 60 * 24 * 365) }, +]; + +const TIME_WINDOW_OPTIONS = [ + { label: "1 minute", value: String(60 * 1000) }, + { label: "5 minutes", value: String(5 * 60 * 1000) }, + { label: "15 minutes", value: String(15 * 60 * 1000) }, + { label: "30 minutes", value: String(30 * 60 * 1000) }, + { label: "1 hour", value: String(60 * 60 * 1000) }, + { label: "1 day", value: String(24 * 60 * 60 * 1000) }, +]; + +const REFILL_INTERVAL_OPTIONS = [ + { label: "1 hour", value: String(60 * 60 * 1000) }, + { label: "6 hours", value: String(6 * 60 * 60 * 1000) }, + { label: "12 hours", value: String(12 * 60 * 60 * 1000) }, + { label: "1 day", value: String(24 * 60 * 60 * 1000) }, + { label: "7 days", value: String(7 * 24 * 60 * 60 * 1000) }, + { label: "30 days", value: String(30 * 24 * 60 * 60 * 1000) }, +]; + +export const AddApiKey = () => { + const [open, setOpen] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [newApiKey, setNewApiKey] = useState(""); + const { refetch } = api.user.get.useQuery(); + const { data: organizations } = api.organization.all.useQuery(); + const createApiKey = api.user.createApiKey.useMutation({ + onSuccess: (data) => { + if (!data) return; + + setNewApiKey(data.key); + setOpen(false); + setShowSuccessModal(true); + form.reset(); + void refetch(); + }, + onError: () => { + toast.error("Failed to generate API key"); + }, + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + prefix: "", + expiresIn: null, + organizationId: "", + rateLimitEnabled: false, + rateLimitTimeWindow: null, + rateLimitMax: null, + remaining: null, + refillAmount: null, + refillInterval: null, + }, + }); + + const rateLimitEnabled = form.watch("rateLimitEnabled"); + + const onSubmit = async (values: FormValues) => { + createApiKey.mutate({ + name: values.name, + expiresIn: values.expiresIn || undefined, + prefix: values.prefix || undefined, + metadata: { + organizationId: values.organizationId, + }, + // Rate limiting + rateLimitEnabled: values.rateLimitEnabled, + rateLimitTimeWindow: values.rateLimitTimeWindow || undefined, + rateLimitMax: values.rateLimitMax || undefined, + // Request limiting + remaining: values.remaining || undefined, + refillAmount: values.refillAmount || undefined, + refillInterval: values.refillInterval || undefined, + }); + }; + + return ( + <> + + + + + + + Generate API Key + + Create a new API key for accessing the API. You can set an + expiration date and a custom prefix for better organization. + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Prefix + + + + + + )} + /> + ( + + Expiration + + + + )} + /> + ( + + Organization + + + + )} + /> + + {/* Rate Limiting Section */} +
+

Rate Limiting

+ ( + +
+ Enable Rate Limiting + + Limit the number of requests within a time window + +
+ + + +
+ )} + /> + + {rateLimitEnabled && ( + <> + ( + + Time Window + + + The duration in which requests are counted + + + + )} + /> + ( + + Maximum Requests + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Maximum number of requests allowed within the time + window + + + + )} + /> + + )} +
+ + {/* Request Limiting Section */} +
+

Request Limiting

+ ( + + Total Request Limit + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Total number of requests allowed (leave empty for + unlimited) + + + + )} + /> + + ( + + Refill Amount + + + field.onChange( + e.target.value + ? Number.parseInt(e.target.value, 10) + : null, + ) + } + /> + + + Number of requests to add on each refill + + + + )} + /> + + ( + + Refill Interval + + + How often to refill the request limit + + + + )} + /> +
+ +
+ + +
+ + +
+
+ + + + + API Key Generated Successfully + + Please copy your API key now. You won't be able to see it again! + + +
+
+ {newApiKey} +
+
+ + +
+
+
+
+ + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx new file mode 100644 index 00000000..6744f1de --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx @@ -0,0 +1,142 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react"; +import Link from "next/link"; +import { toast } from "sonner"; +import { formatDistanceToNow } from "date-fns"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { AddApiKey } from "./add-api-key"; +import { Badge } from "@/components/ui/badge"; + +export const ShowApiKeys = () => { + const { data, refetch } = api.user.get.useQuery(); + const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } = + api.user.deleteApiKey.useMutation(); + + return ( +
+ +
+ +
+ + + API/CLI Keys + + + Generate and manage API keys to access the API/CLI + +
+
+ + Swagger API: + + + View + + +
+
+ +
+ {data?.user.apiKeys && data.user.apiKeys.length > 0 ? ( + data.user.apiKeys.map((apiKey) => ( +
+
+
+ {apiKey.name} +
+ + + Created{" "} + {formatDistanceToNow(new Date(apiKey.createdAt))}{" "} + ago + + {apiKey.prefix && ( + + + {apiKey.prefix} + + )} + {apiKey.expiresAt && ( + + + Expires in{" "} + {formatDistanceToNow( + new Date(apiKey.expiresAt), + )}{" "} + + )} +
+
+ { + try { + await deleteApiKey({ + apiKeyId: apiKey.id, + }); + await refetch(); + toast.success("API key deleted successfully"); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Error deleting API key", + ); + } + }} + > + + +
+
+ )) + ) : ( +
+ + + No API keys found + +
+ )} +
+ + {/* Generate new API key */} +
+ +
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx b/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx deleted file mode 100644 index 5213c0b9..00000000 --- a/apps/dokploy/components/dashboard/settings/profile/generate-token.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { api } from "@/utils/api"; -import { ExternalLinkIcon } from "lucide-react"; -import Link from "next/link"; -import { toast } from "sonner"; - -export const GenerateToken = () => { - const { data, refetch } = api.user.get.useQuery(); - - const { mutateAsync: generateToken, isLoading: isLoadingToken } = - api.user.generateToken.useMutation(); - - return ( -
- -
- -
- API/CLI - - Generate a token to access the API/CLI - -
-
- - Swagger API: - - - View - - -
-
- -
-
-
- - -
-
- -
-
-
-
-
- ); -}; diff --git a/apps/dokploy/drizzle/0066_yielding_echo.sql b/apps/dokploy/drizzle/0066_yielding_echo.sql index a8a2501a..bb5c2511 100644 --- a/apps/dokploy/drizzle/0066_yielding_echo.sql +++ b/apps/dokploy/drizzle/0066_yielding_echo.sql @@ -321,7 +321,6 @@ inserted_admin_members AS ( "user_id", role, "created_at", - "token", "canAccessToAPI", "canAccessToDocker", "canAccessToGitProviders", @@ -340,7 +339,6 @@ inserted_admin_members AS ( a."adminId", 'owner', NOW(), - COALESCE(auth.token, ''), true, -- Los admins tienen todos los permisos por defecto true, true, @@ -364,7 +362,6 @@ INSERT INTO member ( "user_id", role, "created_at", - "token", "canAccessToAPI", "canAccessToDocker", "canAccessToGitProviders", @@ -383,7 +380,6 @@ SELECT u."userId", 'member', NOW(), - COALESCE(auth.token, ''), COALESCE(u."canAccessToAPI", false), COALESCE(u."canAccessToDocker", false), COALESCE(u."canAccessToGitProviders", false), @@ -400,6 +396,29 @@ JOIN admin a ON u."adminId" = a."adminId" JOIN inserted_orgs o ON o."owner_id" = a."adminId" JOIN auth ON auth.id = u."authId"; +-- Migrar tokens de auth a apikey +INSERT INTO apikey ( + id, + name, + key, + user_id, + enabled, + created_at, + updated_at +) +SELECT + gen_random_uuid(), + 'Legacy Token', + auth.token, +user_temp.id, + true, + NOW(), + NOW() +FROM auth +JOIN admin ON auth.id = admin."authId" +JOIN user_temp ON user_temp.id = admin."adminId" +WHERE auth.token IS NOT NULL AND auth.token != ''; + -- Migration tables foreign keys ALTER TABLE "project" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint @@ -436,7 +455,6 @@ ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" ALTER TABLE "server" ADD CONSTRAINT "server_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action; -ALTER TABLE "member" ALTER COLUMN "token" SET DEFAULT '';--> statement-breakpoint ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now(); @@ -635,7 +653,6 @@ ALTER TABLE "git_provider" DROP COLUMN "userId";--> statement-breakpoint ALTER TABLE "server" DROP COLUMN "userId"; -- Drop tables ---> statement-breakpoint DROP TABLE "user" CASCADE;--> statement-breakpoint DROP TABLE "admin" CASCADE;--> statement-breakpoint DROP TABLE "auth" CASCADE;--> statement-breakpoint diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 29a356e0..8bf49b3a 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -470,13 +470,6 @@ "when": 1739426913392, "tag": "0066_yielding_echo", "breakpoints": true - }, - { - "idx": 67, - "version": "7", - "when": 1740860314823, - "tag": "0067_goofy_red_skull", - "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/lib/auth-client.ts b/apps/dokploy/lib/auth-client.ts index 9a184959..f1088e73 100644 --- a/apps/dokploy/lib/auth-client.ts +++ b/apps/dokploy/lib/auth-client.ts @@ -1,8 +1,9 @@ import { organizationClient } from "better-auth/client/plugins"; import { twoFactorClient } from "better-auth/client/plugins"; +import { apiKeyClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ // baseURL: "http://localhost:3000", // the base url of your auth server - plugins: [organizationClient(), twoFactorClient()], + plugins: [organizationClient(), twoFactorClient(), apiKeyClient()], }); diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index a1626fc4..3c7b0528 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -36,7 +36,7 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { - "better-auth": "beta", + "better-auth": "1.2.0", "bl": "6.0.11", "rotating-file-stream": "3.2.3", "qrcode": "^1.5.3", diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 404d400c..83ff5624 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -1,4 +1,4 @@ -import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token"; +import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; @@ -19,7 +19,7 @@ const Page = () => {
- {(data?.canAccessToAPI || data?.role === "owner") && } + {(data?.canAccessToAPI || data?.role === "owner") && } {/* {isCloud && } */}
diff --git a/apps/dokploy/pages/swagger.tsx b/apps/dokploy/pages/swagger.tsx index 3d8cc01d..11ea0731 100644 --- a/apps/dokploy/pages/swagger.tsx +++ b/apps/dokploy/pages/swagger.tsx @@ -30,7 +30,41 @@ const Home: NextPage = () => { return (
- + (args: any) => { + const result = ori(args); + const apiKey = args?.apiKey?.value; + if (apiKey) { + localStorage.setItem("swagger_api_key", apiKey); + } + return result; + }, + logout: (ori: any) => (args: any) => { + const result = ori(args); + localStorage.removeItem("swagger_api_key"); + return result; + }, + }, + }, + }, + }, + ]} + requestInterceptor={(request: any) => { + const apiKey = localStorage.getItem("swagger_api_key"); + if (apiKey) { + request.headers = request.headers || {}; + request.headers["x-api-key"] = apiKey; + } + return request; + }} + />
); }; diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index d2455fdb..fc1255fc 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -482,10 +482,28 @@ export const settingsRouter = createTRPCRouter({ openApiDocument.info = { title: "Dokploy API", description: "Endpoints for dokploy", - // TODO: get version from package.json version: "1.0.0", }; + // Add security schemes configuration + openApiDocument.components = { + ...openApiDocument.components, + securitySchemes: { + apiKey: { + type: "apiKey", + in: "header", + name: "x-api-key", + description: "API key authentication", + }, + }, + }; + + // Apply security globally to all endpoints + openApiDocument.security = [ + { + apiKey: [], + }, + ]; return openApiDocument; }, ), diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 1dac65fe..5a84742a 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -5,6 +5,7 @@ import { getUserByToken, removeUserById, updateUser, + createApiKey, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -14,6 +15,7 @@ import { apiUpdateUser, invitation, member, + apikey, } from "@dokploy/server/db/schema"; import * as bcrypt from "bcrypt"; import { TRPCError } from "@trpc/server"; @@ -25,6 +27,24 @@ import { protectedProcedure, publicProcedure, } from "../trpc"; + +const apiCreateApiKey = z.object({ + name: z.string().min(1), + prefix: z.string().optional(), + expiresIn: z.number().optional(), + metadata: z.object({ + organizationId: z.string(), + }), + // Rate limiting + rateLimitEnabled: z.boolean().optional(), + rateLimitTimeWindow: z.number().optional(), + rateLimitMax: z.number().optional(), + // Request limiting + remaining: z.number().optional(), + refillAmount: z.number().optional(), + refillInterval: z.number().optional(), +}); + export const userRouter = createTRPCRouter({ all: adminProcedure.query(async ({ ctx }) => { return await db.query.member.findMany({ @@ -61,7 +81,11 @@ export const userRouter = createTRPCRouter({ eq(member.organizationId, ctx.session?.activeOrganizationId || ""), ), with: { - user: true, + user: { + with: { + apiKeys: true, + }, + }, }, }); @@ -249,4 +273,44 @@ export const userRouter = createTRPCRouter({ generateToken: protectedProcedure.mutation(async () => { return "token"; }), + + deleteApiKey: protectedProcedure + .input( + z.object({ + apiKeyId: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + try { + const apiKeyToDelete = await db.query.apikey.findFirst({ + where: eq(apikey.id, input.apiKeyId), + }); + + if (!apiKeyToDelete) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "API key not found", + }); + } + + if (apiKeyToDelete.userId !== ctx.user.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this API key", + }); + } + + await db.delete(apikey).where(eq(apikey.id, input.apiKeyId)); + return true; + } catch (error) { + throw error; + } + }), + + createApiKey: protectedProcedure + .input(apiCreateApiKey) + .mutation(async ({ input, ctx }) => { + const apiKey = await createApiKey(ctx.user.id, input); + return apiKey; + }), }); diff --git a/packages/server/package.json b/packages/server/package.json index bd2d9ed0..d99f5c24 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -32,7 +32,7 @@ "@oslojs/encoding":"1.1.0", "@oslojs/crypto":"1.0.1", "drizzle-dbml-generator":"0.10.0", - "better-auth":"beta", + "better-auth":"1.2.0", "rotating-file-stream": "3.2.3", "@faker-js/faker": "^8.4.1", "@lucia-auth/adapter-drizzle": "1.0.7", diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index e5f2dab7..8291ea4d 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -185,3 +185,10 @@ export const apikey = pgTable("apikey", { permissions: text("permissions"), metadata: text("metadata"), }); + +export const apikeyRelations = relations(apikey, ({ one }) => ({ + user: one(users_temp, { + fields: [apikey.userId], + references: [users_temp.id], + }), +})); diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 3916f1e7..9307127a 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -10,7 +10,7 @@ import { import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { account, organization } from "./account"; +import { account, organization, apikey } from "./account"; import { projects } from "./project"; import { certificateType } from "./shared"; /** @@ -123,6 +123,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({ }), organizations: many(organization), projects: many(projects), + apiKeys: many(apikey), })); const createSchema = createInsertSchema(users_temp, { diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index bebcd54e..3089bb1d 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -9,7 +9,7 @@ import * as schema from "../db/schema"; import { sendEmail } from "../verification/send-verification-email"; import { IS_CLOUD } from "../constants"; -export const auth = betterAuth({ +const { handler, api } = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: schema, @@ -126,7 +126,9 @@ export const auth = betterAuth({ }, plugins: [ - apiKey(), + apiKey({ + enableMetadata: true, + }), twoFactor(), organization({ async sendInvitationEmail(data, _request) { @@ -145,11 +147,111 @@ export const auth = betterAuth({ ], }); -// export const auth = { -// handler, -// }; +export const auth = { + handler, + api, +}; export const validateRequest = async (request: IncomingMessage) => { + const apiKey = request.headers["x-api-key"] as string; + if (apiKey) { + try { + const { valid, key, error } = await api.verifyApiKey({ + body: { + key: apiKey, + }, + }); + + if (error) { + throw new Error(error.message || "Error verifying API key"); + } + if (!valid || !key) { + return { + session: null, + user: null, + }; + } + + const apiKeyRecord = await db.query.apikey.findFirst({ + where: eq(schema.apikey.id, key.id), + with: { + user: true, + }, + }); + + if (!apiKeyRecord) { + return { + session: null, + user: null, + }; + } + + const organizationId = JSON.parse( + apiKeyRecord.metadata || "{}", + ).organizationId; + + if (!organizationId) { + return { + session: null, + user: null, + }; + } + + const member = await db.query.member.findFirst({ + where: and( + eq(schema.member.userId, apiKeyRecord.user.id), + eq(schema.member.organizationId, organizationId), + ), + with: { + organization: true, + }, + }); + + const { + id, + name, + email, + emailVerified, + image, + createdAt, + updatedAt, + twoFactorEnabled, + } = apiKeyRecord.user; + + const mockSession = { + session: { + user: { + id: apiKeyRecord.user.id, + email: apiKeyRecord.user.email, + name: apiKeyRecord.user.name, + }, + activeOrganizationId: organizationId || "", + }, + user: { + id, + name, + email, + emailVerified, + image, + createdAt, + updatedAt, + twoFactorEnabled, + role: member?.role || "member", + ownerId: member?.organization.ownerId || apiKeyRecord.user.id, + }, + }; + + return mockSession; + } catch (error) { + console.error("Error verifying API key", error); + return { + session: null, + user: null, + }; + } + } + + // If no API key, proceed with normal session validation const session = await api.getSession({ headers: new Headers({ cookie: request.headers.cookie || "", diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index a1901d71..312753ff 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -1,7 +1,8 @@ import { db } from "@dokploy/server/db"; -import { member, users_temp } from "@dokploy/server/db/schema"; +import { apikey, member, users_temp } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { and, eq } from "drizzle-orm"; +import { auth } from "../lib/auth"; export type User = typeof users_temp.$inferSelect; @@ -248,3 +249,46 @@ export const updateUser = async (userId: string, userData: Partial) => { return user; }; + +export const createApiKey = async ( + userId: string, + input: { + name: string; + prefix?: string; + expiresIn?: number; + metadata: { + organizationId: string; + }; + rateLimitEnabled?: boolean; + rateLimitTimeWindow?: number; + rateLimitMax?: number; + remaining?: number; + refillAmount?: number; + refillInterval?: number; + }, +) => { + const apiKey = await auth.api.createApiKey({ + body: { + name: input.name, + expiresIn: input.expiresIn, + prefix: input.prefix, + rateLimitEnabled: input.rateLimitEnabled, + rateLimitTimeWindow: input.rateLimitTimeWindow, + rateLimitMax: input.rateLimitMax, + remaining: input.remaining, + refillAmount: input.refillAmount, + refillInterval: input.refillInterval, + userId, + }, + }); + + if (input.metadata) { + await db + .update(apikey) + .set({ + metadata: JSON.stringify(input.metadata), + }) + .where(eq(apikey.id, apiKey.id)); + } + return apiKey; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e1c088b..03fb11f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -239,8 +239,8 @@ importers: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) better-auth: - specifier: beta - version: 1.2.0-beta.18(typescript@5.5.3) + specifier: 1.2.0 + version: 1.2.0(typescript@5.5.3) bl: specifier: 6.0.11 version: 6.0.11 @@ -580,8 +580,8 @@ importers: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) better-auth: - specifier: beta - version: 1.2.0-beta.18(typescript@5.5.3) + specifier: 1.2.0 + version: 1.2.0(typescript@5.5.3) bl: specifier: 6.0.11 version: 6.0.11 @@ -3879,8 +3879,8 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - better-auth@1.2.0-beta.18: - resolution: {integrity: sha512-gEjNxmrkFiATTSTcE47rkyTT9vMFMLTjtLNun4W0IWmeqfi4pIbbWpo97foY1DNXXRDkDuajquoD58dzAatQxQ==} + better-auth@1.2.0: + resolution: {integrity: sha512-eIRGOXfix25bh4fgs8jslZAZssufpIkxfEeEokQu5G4wICoDee1wPctkFb8v80PvhtI4dPm28SuAoZaAdRc6Wg==} better-call@1.0.3: resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==} @@ -10554,7 +10554,7 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.2.0-beta.18(typescript@5.5.3): + better-auth@1.2.0(typescript@5.5.3): dependencies: '@better-auth/utils': 0.2.3 '@better-fetch/fetch': 1.1.15