better-auth
Version:
The most comprehensive authentication framework for TypeScript.
250 lines (248 loc) • 11 kB
JavaScript
import { getDate } from "../../../utils/date.mjs";
import { getSessionFromCtx } from "../../../api/routes/session.mjs";
import { generateId as generateId$1 } from "../../../utils/index.mjs";
import { APIError } from "../../../api/index.mjs";
import { setApiKey } from "../adapter.mjs";
import { API_KEY_TABLE_NAME, ERROR_CODES, defaultKeyHasher } from "../index.mjs";
import { safeJSONParse } from "@better-auth/core/utils";
import * as z from "zod";
import { createAuthEndpoint } from "@better-auth/core/api";
//#region src/plugins/api-key/routes/create-api-key.ts
const createApiKeyBodySchema = z.object({
name: z.string().meta({ description: "Name of the Api Key" }).optional(),
expiresIn: z.number().meta({ description: "Expiration time of the Api Key in seconds" }).min(1).optional().nullable().default(null),
userId: z.coerce.string().meta({ description: "User Id of the user that the Api Key belongs to. server-only. Eg: \"user-id\"" }).optional(),
prefix: z.string().meta({ description: "Prefix of the Api Key" }).regex(/^[a-zA-Z0-9_-]+$/, { message: "Invalid prefix format, must be alphanumeric and contain only underscores and hyphens." }).optional(),
remaining: z.number().meta({ description: "Remaining number of requests. Server side only" }).min(0).optional().nullable().default(null),
metadata: z.any().optional(),
refillAmount: z.number().meta({ description: "Amount to refill the remaining count of the Api Key. server-only. Eg: 100" }).min(1).optional(),
refillInterval: z.number().meta({ description: "Interval to refill the Api Key in milliseconds. server-only. Eg: 1000" }).optional(),
rateLimitTimeWindow: z.number().meta({ description: "The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 1000" }).optional(),
rateLimitMax: z.number().meta({ description: "Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100" }).optional(),
rateLimitEnabled: z.boolean().meta({ description: "Whether the key has rate limiting enabled. server-only. Eg: true" }).optional(),
permissions: z.record(z.string(), z.array(z.string())).meta({ description: "Permissions of the Api Key." }).optional()
});
function createApiKey({ keyGenerator, opts, schema, deleteAllExpiredApiKeys }) {
return createAuthEndpoint("/api-key/create", {
method: "POST",
body: createApiKeyBodySchema,
metadata: { openapi: {
description: "Create a new API key for a user",
responses: { "200": {
description: "API key created successfully",
content: { "application/json": { schema: {
type: "object",
properties: {
id: {
type: "string",
description: "Unique identifier of the API key"
},
createdAt: {
type: "string",
format: "date-time",
description: "Creation timestamp"
},
updatedAt: {
type: "string",
format: "date-time",
description: "Last update timestamp"
},
name: {
type: "string",
nullable: true,
description: "Name of the API key"
},
prefix: {
type: "string",
nullable: true,
description: "Prefix of the API key"
},
start: {
type: "string",
nullable: true,
description: "Starting characters of the key (if configured)"
},
key: {
type: "string",
description: "The full API key (only returned on creation)"
},
enabled: {
type: "boolean",
description: "Whether the key is enabled"
},
expiresAt: {
type: "string",
format: "date-time",
nullable: true,
description: "Expiration timestamp"
},
userId: {
type: "string",
description: "ID of the user owning the key"
},
lastRefillAt: {
type: "string",
format: "date-time",
nullable: true,
description: "Last refill timestamp"
},
lastRequest: {
type: "string",
format: "date-time",
nullable: true,
description: "Last request timestamp"
},
metadata: {
type: "object",
nullable: true,
additionalProperties: true,
description: "Metadata associated with the key"
},
rateLimitMax: {
type: "number",
nullable: true,
description: "Maximum requests in time window"
},
rateLimitTimeWindow: {
type: "number",
nullable: true,
description: "Rate limit time window in milliseconds"
},
remaining: {
type: "number",
nullable: true,
description: "Remaining requests"
},
refillAmount: {
type: "number",
nullable: true,
description: "Amount to refill"
},
refillInterval: {
type: "number",
nullable: true,
description: "Refill interval in milliseconds"
},
rateLimitEnabled: {
type: "boolean",
description: "Whether rate limiting is enabled"
},
requestCount: {
type: "number",
description: "Current request count in window"
},
permissions: {
type: "object",
nullable: true,
additionalProperties: {
type: "array",
items: { type: "string" }
},
description: "Permissions associated with the key"
}
},
required: [
"id",
"createdAt",
"updatedAt",
"key",
"enabled",
"userId",
"rateLimitEnabled",
"requestCount"
]
} } }
} }
} }
}, async (ctx) => {
const { name, expiresIn, prefix, remaining, metadata, refillAmount, refillInterval, permissions, rateLimitMax, rateLimitTimeWindow, rateLimitEnabled } = ctx.body;
const session = await getSessionFromCtx(ctx);
const authRequired = ctx.request || ctx.headers;
const user = authRequired && !session ? null : session?.user || { id: ctx.body.userId };
if (!user?.id) throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.UNAUTHORIZED_SESSION });
if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.UNAUTHORIZED_SESSION });
if (authRequired) {
if (refillAmount !== void 0 || refillInterval !== void 0 || rateLimitMax !== void 0 || rateLimitTimeWindow !== void 0 || rateLimitEnabled !== void 0 || permissions !== void 0 || remaining !== null) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.SERVER_ONLY_PROPERTY });
}
if (metadata) {
if (opts.enableMetadata === false) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.METADATA_DISABLED });
if (typeof metadata !== "object") throw new APIError("BAD_REQUEST", { message: ERROR_CODES.INVALID_METADATA_TYPE });
}
if (refillAmount && !refillInterval) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED });
if (refillInterval && !refillAmount) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED });
if (expiresIn) {
if (opts.keyExpiration.disableCustomExpiresTime === true) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.KEY_DISABLED_EXPIRATION });
const expiresIn_in_days = expiresIn / (3600 * 24);
if (opts.keyExpiration.minExpiresIn > expiresIn_in_days) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL });
else if (opts.keyExpiration.maxExpiresIn < expiresIn_in_days) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE });
}
if (prefix) {
if (prefix.length < opts.minimumPrefixLength) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.INVALID_PREFIX_LENGTH });
if (prefix.length > opts.maximumPrefixLength) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.INVALID_PREFIX_LENGTH });
}
if (name) {
if (name.length < opts.minimumNameLength) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.INVALID_NAME_LENGTH });
if (name.length > opts.maximumNameLength) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.INVALID_NAME_LENGTH });
} else if (opts.requireName) throw new APIError("BAD_REQUEST", { message: ERROR_CODES.NAME_REQUIRED });
deleteAllExpiredApiKeys(ctx.context);
const key = await keyGenerator({
length: opts.defaultKeyLength,
prefix: prefix || opts.defaultPrefix
});
const hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);
let start = null;
if (opts.startingCharactersConfig.shouldStore) start = key.substring(0, opts.startingCharactersConfig.charactersLength);
const defaultPermissions = opts.permissions?.defaultPermissions ? typeof opts.permissions.defaultPermissions === "function" ? await opts.permissions.defaultPermissions(user.id, ctx) : opts.permissions.defaultPermissions : void 0;
const permissionsToApply = permissions ? JSON.stringify(permissions) : defaultPermissions ? JSON.stringify(defaultPermissions) : void 0;
let data = {
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date(),
name: name ?? null,
prefix: prefix ?? opts.defaultPrefix ?? null,
start,
key: hashed,
enabled: true,
expiresAt: expiresIn ? getDate(expiresIn, "sec") : opts.keyExpiration.defaultExpiresIn ? getDate(opts.keyExpiration.defaultExpiresIn, "sec") : null,
userId: user.id,
lastRefillAt: null,
lastRequest: null,
metadata: null,
rateLimitMax: rateLimitMax ?? opts.rateLimit.maxRequests ?? null,
rateLimitTimeWindow: rateLimitTimeWindow ?? opts.rateLimit.timeWindow ?? null,
remaining: remaining === null ? remaining : remaining ?? refillAmount ?? null,
refillAmount: refillAmount ?? null,
refillInterval: refillInterval ?? null,
rateLimitEnabled: rateLimitEnabled === void 0 ? opts.rateLimit.enabled ?? true : rateLimitEnabled,
requestCount: 0,
permissions: permissionsToApply
};
if (metadata) data.metadata = schema.apikey.fields.metadata.transform.input(metadata);
let apiKey;
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
apiKey = await ctx.context.adapter.create({
model: API_KEY_TABLE_NAME,
data
});
await setApiKey(ctx, apiKey, opts);
} else if (opts.storage === "secondary-storage") {
const id = ctx.context.generateId({ model: API_KEY_TABLE_NAME }) ?? generateId$1();
apiKey = {
...data,
id
};
await setApiKey(ctx, apiKey, opts);
} else apiKey = await ctx.context.adapter.create({
model: API_KEY_TABLE_NAME,
data
});
return ctx.json({
...apiKey,
key,
metadata: metadata ?? null,
permissions: apiKey.permissions ? safeJSONParse(apiKey.permissions) : null
});
});
}
//#endregion
export { createApiKey };
//# sourceMappingURL=create-api-key.mjs.map