feat(api): implement advanced API key management with granular controls

This commit is contained in:
Mauricio Siu
2025-03-01 19:58:15 -06:00
parent 5568629839
commit 5dc5292928
17 changed files with 926 additions and 112 deletions

View File

@@ -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],
}),
}));

View File

@@ -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, {

View File

@@ -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 || "",

View File

@@ -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<User>) => {
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;
};