From afecf4ce41a0b4c58ff7e3d5e77b68397abcfc78 Mon Sep 17 00:00:00 2001 From: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:25:39 +0300 Subject: [PATCH] feat: custom table names and fields for plugins (#570) --- docs/content/docs/concepts/database.mdx | 21 ++++ docs/content/docs/concepts/plugins.mdx | 2 +- docs/content/docs/concepts/rate-limit.mdx | 2 +- docs/content/docs/plugins/organization.mdx | 19 ++++ .../src/__snapshots__/init.test.ts.snap | 8 +- .../drizzle-adapter/drizzle-adapter.ts | 4 +- .../adapters/kysely-adapter/kysely-adapter.ts | 2 +- .../mongodb-adapter/mongodb-adapter.ts | 2 +- .../adapters/prisma-adapter/prisma-adapter.ts | 2 +- .../better-auth/src/api/rate-limiter/index.ts | 10 +- packages/better-auth/src/client/query.ts | 1 + packages/better-auth/src/db/field.ts | 26 ++--- packages/better-auth/src/db/get-schema.ts | 8 +- packages/better-auth/src/db/get-tables.ts | 16 +-- packages/better-auth/src/db/schema.ts | 32 +++++- .../src/plugins/admin/admin.test.ts | 12 +- .../better-auth/src/plugins/admin/index.ts | 88 ++++++++------ .../src/plugins/anonymous/anon.test.ts | 7 ++ .../src/plugins/anonymous/index.ts | 42 ++++--- packages/better-auth/src/plugins/jwt/index.ts | 9 +- .../better-auth/src/plugins/jwt/schema.ts | 4 +- .../src/plugins/organization/adapter.ts | 10 +- .../better-auth/src/plugins/passkey/index.ts | 107 ++++++++++-------- .../src/plugins/phone-number/index.ts | 52 +++++---- .../src/plugins/two-factor/index.ts | 42 +------ .../src/plugins/two-factor/schema.ts | 37 ++++++ .../src/plugins/two-factor/types.ts | 11 +- .../better-auth/src/plugins/username/index.ts | 4 +- packages/better-auth/src/types/options.ts | 2 +- packages/better-auth/src/types/plugins.ts | 16 ++- packages/cli/src/generators/drizzle.ts | 4 +- packages/cli/src/generators/prisma.ts | 20 ++-- 32 files changed, 388 insertions(+), 234 deletions(-) create mode 100644 packages/better-auth/src/plugins/two-factor/schema.ts diff --git a/docs/content/docs/concepts/database.mdx b/docs/content/docs/concepts/database.mdx index 6100a533..ee677f1d 100644 --- a/docs/content/docs/concepts/database.mdx +++ b/docs/content/docs/concepts/database.mdx @@ -470,6 +470,27 @@ export const auth = betterAuth({ Type inference in your code will still use the original field names (e.g., `user.name`, not `user.full_name`). +To customize table names and column name for plugins, you can use the `schema` property in the plugin config: + +```ts title="auth.ts" +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + plugins: { + twoFactor: { + schema: { + user: { + fields: { + twoFactorEnabled: "two_factor_enabled", + twoFactorSecret: "two_factor_secret" + } + } + } + } + } +}) +``` + ### Extending Core Schema diff --git a/docs/content/docs/concepts/plugins.mdx b/docs/content/docs/concepts/plugins.mdx index ce17f41f..cb65d52e 100644 --- a/docs/content/docs/concepts/plugins.mdx +++ b/docs/content/docs/concepts/plugins.mdx @@ -118,7 +118,7 @@ const myPlugin = ()=> { type: "string" } }, - tableName: "myTable" // optional if you want to use a different name than the key + modelName: "myTable" // optional if you want to use a different name than the key } } } satisfies BetterAuthPlugin diff --git a/docs/content/docs/concepts/rate-limit.mdx b/docs/content/docs/concepts/rate-limit.mdx index ab9ab674..67244f26 100644 --- a/docs/content/docs/concepts/rate-limit.mdx +++ b/docs/content/docs/concepts/rate-limit.mdx @@ -64,7 +64,7 @@ export const auth = betterAuth({ //...other options rateLimit: { storage: "database", - tableName: "rateLimit", //optional by default "rateLimit" is used + modelName: "rateLimit", //optional by default "rateLimit" is used }, }) ``` diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index 4edf8a63..ad3c6c91 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -655,6 +655,25 @@ Table Name: `invitation` ]} /> +### Customizing the Schema + +To change the schema table name or fields, you can pass `schema` option to the organization plugin. + +```ts title="auth.ts" +const auth = betterAuth({ + plugins: [organization({ + schema: { + organization: { + modelName: "organizations", //map the organization table to organizations + fields: { + name: "title" //map the name field to title + } + } + } + })] +}) +``` + ## Options **allowUserToCreateOrganization**: `boolean` | `((user: User) => Promise | boolean)` - A function that determines whether a user can create an organization. By default, it's `true`. You can set it to `false` to restrict users from creating organizations. diff --git a/packages/better-auth/src/__snapshots__/init.test.ts.snap b/packages/better-auth/src/__snapshots__/init.test.ts.snap index 04c0fb41..9e43fa32 100644 --- a/packages/better-auth/src/__snapshots__/init.test.ts.snap +++ b/packages/better-auth/src/__snapshots__/init.test.ts.snap @@ -174,8 +174,8 @@ exports[`init > should match config 1`] = ` "type": "string", }, }, + "modelName": "account", "order": 3, - "tableName": "account", }, "session": { "fields": { @@ -205,8 +205,8 @@ exports[`init > should match config 1`] = ` "type": "string", }, }, + "modelName": "session", "order": 2, - "tableName": "session", }, "user": { "fields": { @@ -245,8 +245,8 @@ exports[`init > should match config 1`] = ` "type": "date", }, }, + "modelName": "user", "order": 1, - "tableName": "user", }, "verification": { "fields": { @@ -272,8 +272,8 @@ exports[`init > should match config 1`] = ` "type": "string", }, }, + "modelName": "verification", "order": 4, - "tableName": "verification", }, }, "trustedOrigins": [ diff --git a/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts b/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts index dbf011e6..28ddddb9 100644 --- a/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts +++ b/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts @@ -40,8 +40,8 @@ const createTransform = ( } const getModelName = (model: string) => { - return schema[model].tableName !== model - ? schema[model].tableName + return schema[model].modelName !== model + ? schema[model].modelName : config.usePlural ? `${model}s` : model; diff --git a/packages/better-auth/src/adapters/kysely-adapter/kysely-adapter.ts b/packages/better-auth/src/adapters/kysely-adapter/kysely-adapter.ts index 451db8d4..b642b9f8 100644 --- a/packages/better-auth/src/adapters/kysely-adapter/kysely-adapter.ts +++ b/packages/better-auth/src/adapters/kysely-adapter/kysely-adapter.ts @@ -83,7 +83,7 @@ const createTransform = ( } function getModelName(model: string) { - return schema[model].tableName; + return schema[model].modelName; } const shouldGenerateId = config?.generateId !== false; diff --git a/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts b/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts index 420b9461..52bc1da2 100644 --- a/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts +++ b/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts @@ -183,7 +183,7 @@ const createTransform = (options: BetterAuthOptions) => { return clause; }, getModelName: (model: string) => { - return schema[model].tableName; + return schema[model].modelName; }, getField, }; diff --git a/packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts b/packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts index 9fafe08f..8108b826 100644 --- a/packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts +++ b/packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts @@ -59,7 +59,7 @@ const createTransform = (config: PrismaConfig, options: BetterAuthOptions) => { } function getModelName(model: string) { - return schema[model].tableName; + return schema[model].modelName; } const shouldGenerateId = config?.generateId !== false; return { diff --git a/packages/better-auth/src/api/rate-limiter/index.ts b/packages/better-auth/src/api/rate-limiter/index.ts index eff09773..0f3d6579 100644 --- a/packages/better-auth/src/api/rate-limiter/index.ts +++ b/packages/better-auth/src/api/rate-limiter/index.ts @@ -33,8 +33,8 @@ function getRetryAfter(lastRequest: number, window: number) { return Math.ceil((lastRequest + windowInMs - now) / 1000); } -function createDBStorage(ctx: AuthContext, tableName?: string) { - const model = tableName ?? "rateLimit"; +function createDBStorage(ctx: AuthContext, modelName?: string) { + const model = "rateLimit"; const db = ctx.adapter; return { get: async (key: string) => { @@ -48,7 +48,7 @@ function createDBStorage(ctx: AuthContext, tableName?: string) { try { if (_update) { await db.update({ - model: tableName ?? "rateLimit", + model: modelName ?? "rateLimit", where: [{ field: "key", value: key }], update: { count: value.count, @@ -57,7 +57,7 @@ function createDBStorage(ctx: AuthContext, tableName?: string) { }); } else { await db.create({ - model: tableName ?? "rateLimit", + model: modelName ?? "rateLimit", data: { key, count: value.count, @@ -96,7 +96,7 @@ export function getRateLimitStorage(ctx: AuthContext) { }, }; } - return createDBStorage(ctx, ctx.rateLimit.tableName); + return createDBStorage(ctx, ctx.rateLimit.modelName); } export async function onRequestRateLimit(req: Request, ctx: AuthContext) { diff --git a/packages/better-auth/src/client/query.ts b/packages/better-auth/src/client/query.ts index 3dc8ba70..ad19d900 100644 --- a/packages/better-auth/src/client/query.ts +++ b/packages/better-auth/src/client/query.ts @@ -61,6 +61,7 @@ export const useAuthQuery = ( }); await opts?.onError?.(context); }, + async onRequest(context) { const currentValue = value.get(); value.set({ diff --git a/packages/better-auth/src/db/field.ts b/packages/better-auth/src/db/field.ts index c6f41a91..abc8cadb 100644 --- a/packages/better-auth/src/db/field.ts +++ b/packages/better-auth/src/db/field.ts @@ -8,6 +8,8 @@ export type FieldType = | "date" | `${"string" | "number"}[]`; +type Primitive = string | number | boolean | Date | null | undefined; + export type FieldAttributeConfig = { /** * If the field should be required on a new record. @@ -24,22 +26,22 @@ export type FieldAttributeConfig = { * @default true */ input?: boolean; - /** - * If the value should be hashed when it's stored. - * @default false - */ - hashValue?: boolean; /** * Default value for the field * * Note: This will not create a default value on the database level. It will only * be used when creating a new record. */ - defaultValue?: any; + defaultValue?: Primitive | (() => Primitive); /** * transform the value before storing it. */ - transform?: (value: InferValueType) => InferValueType; + transform?: { + input?: (value: InferValueType) => Primitive | Promise; + output?: ( + value: Primitive, + ) => InferValueType | Promise>; + }; /** * Reference to another model. */ @@ -67,14 +69,12 @@ export type FieldAttributeConfig = { /** * A zod schema to validate the value. */ - validator?: ZodSchema; + validator?: { + input?: ZodSchema; + output?: ZodSchema; + }; /** * The name of the field on the database. - * - * @default - * ```txt - * the key in the fields object. - * ``` */ fieldName?: string; }; diff --git a/packages/better-auth/src/db/get-schema.ts b/packages/better-auth/src/db/get-schema.ts index 6741fa53..a6707ef2 100644 --- a/packages/better-auth/src/db/get-schema.ts +++ b/packages/better-auth/src/db/get-schema.ts @@ -17,14 +17,14 @@ export function getSchema(config: BetterAuthOptions) { Object.entries(fields).forEach(([key, field]) => { actualFields[field.fieldName || key] = field; }); - if (schema[table.tableName]) { - schema[table.tableName].fields = { - ...schema[table.tableName].fields, + if (schema[table.modelName]) { + schema[table.modelName].fields = { + ...schema[table.modelName].fields, ...actualFields, }; continue; } - schema[table.tableName] = { + schema[table.modelName] = { fields: actualFields, order: table.order || Infinity, }; diff --git a/packages/better-auth/src/db/get-tables.ts b/packages/better-auth/src/db/get-tables.ts index c33d2794..c529680b 100644 --- a/packages/better-auth/src/db/get-tables.ts +++ b/packages/better-auth/src/db/get-tables.ts @@ -7,7 +7,7 @@ export type BetterAuthDbSchema = Record< /** * The name of the table in the database */ - tableName: string; + modelName: string; /** * The fields of the table */ @@ -37,21 +37,21 @@ export const getAuthTables = ( ...acc[key]?.fields, ...value.fields, }, - tableName: value.tableName || key, + modelName: value.modelName || key, }; } return acc; }, {} as Record< string, - { fields: Record; tableName: string } + { fields: Record; modelName: string } >, ); const shouldAddRateLimitTable = options.rateLimit?.storage === "database"; const rateLimitTable = { rateLimit: { - tableName: options.rateLimit?.tableName || "rateLimit", + modelName: options.rateLimit?.modelName || "rateLimit", fields: { key: { type: "string", @@ -72,7 +72,7 @@ export const getAuthTables = ( const { user, session, account, ...pluginTables } = pluginSchema || {}; return { user: { - tableName: options.user?.modelName || "user", + modelName: options.user?.modelName || "user", fields: { name: { type: "string", @@ -114,7 +114,7 @@ export const getAuthTables = ( order: 1, }, session: { - tableName: options.session?.modelName || "session", + modelName: options.session?.modelName || "session", fields: { expiresAt: { type: "date", @@ -147,7 +147,7 @@ export const getAuthTables = ( order: 2, }, account: { - tableName: options.account?.modelName || "account", + modelName: options.account?.modelName || "account", fields: { accountId: { type: "string", @@ -199,7 +199,7 @@ export const getAuthTables = ( order: 3, }, verification: { - tableName: options.verification?.modelName || "verification", + modelName: options.verification?.modelName || "verification", fields: { identifier: { type: "string", diff --git a/packages/better-auth/src/db/schema.ts b/packages/better-auth/src/db/schema.ts index c42793cb..46c5ace0 100644 --- a/packages/better-auth/src/db/schema.ts +++ b/packages/better-auth/src/db/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { FieldAttribute } from "."; -import type { BetterAuthOptions } from "../types"; +import type { BetterAuthOptions, PluginSchema } from "../types"; export const accountSchema = z.object({ id: z.string(), @@ -172,3 +172,33 @@ export function parseSessionInput( const schema = getAllFields(options, "session"); return parseInputData(session, { fields: schema }); } + +export function mergeSchema( + schema: S, + newSchema?: { + [K in keyof S]?: { + modelName?: string; + fields?: { + [P: string]: string; + }; + }; + }, +) { + if (!newSchema) { + return schema; + } + for (const table in newSchema) { + const newModelName = newSchema[table]?.modelName; + if (newModelName) { + schema[table].modelName = newModelName; + } + for (const field in schema[table].fields) { + const newField = newSchema[table]?.fields?.[field]; + if (!newField) { + continue; + } + schema[table].fields[field].fieldName = newField; + } + } + return schema; +} diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts index 86360135..19805d28 100644 --- a/packages/better-auth/src/plugins/admin/admin.test.ts +++ b/packages/better-auth/src/plugins/admin/admin.test.ts @@ -6,7 +6,17 @@ import { adminClient } from "./client"; describe("Admin plugin", async () => { const { client, signInWithTestUser } = await getTestInstance( { - plugins: [admin()], + plugins: [ + admin({ + schema: { + user: { + fields: { + role: "_role", + }, + }, + }, + }), + ], logger: { level: "error", }, diff --git a/packages/better-auth/src/plugins/admin/index.ts b/packages/better-auth/src/plugins/admin/index.ts index 77620e00..a57e17bc 100644 --- a/packages/better-auth/src/plugins/admin/index.ts +++ b/packages/better-auth/src/plugins/admin/index.ts @@ -5,9 +5,17 @@ import { createAuthMiddleware, getSessionFromCtx, } from "../../api"; -import type { BetterAuthPlugin, Session, User, Where } from "../../types"; +import { + type BetterAuthPlugin, + type InferOptionSchema, + type PluginSchema, + type Session, + type User, + type Where, +} from "../../types"; import { setSessionCookie } from "../../cookies"; import { getDate } from "../../utils/date"; +import { mergeSchema } from "../../db/schema"; export interface UserWithRole extends User { role?: string | null; @@ -53,9 +61,13 @@ interface AdminOptions { * By default, the impersonation session lasts 1 hour */ impersonationSessionDuration?: number; + /** + * Custom schema for the admin plugin + */ + schema?: InferOptionSchema; } -export const admin = (options?: AdminOptions) => { +export const admin = (options?: O) => { const opts = { defaultRole: "user", adminRole: "admin", @@ -478,40 +490,42 @@ export const admin = (options?: AdminOptions) => { }, ), }, - schema: { - user: { - fields: { - role: { - type: "string", - required: false, - input: false, - }, - banned: { - type: "boolean", - defaultValue: false, - required: false, - input: false, - }, - banReason: { - type: "string", - required: false, - input: false, - }, - banExpires: { - type: "date", - required: false, - input: false, - }, - }, - }, - session: { - fields: { - impersonatedBy: { - type: "string", - required: false, - }, - }, - }, - }, + schema: mergeSchema(schema, opts.schema), } satisfies BetterAuthPlugin; }; + +const schema = { + user: { + fields: { + role: { + type: "string", + required: false, + input: false, + }, + banned: { + type: "boolean", + defaultValue: false, + required: false, + input: false, + }, + banReason: { + type: "string", + required: false, + input: false, + }, + banExpires: { + type: "date", + required: false, + input: false, + }, + }, + }, + session: { + fields: { + impersonatedBy: { + type: "string", + required: false, + }, + }, + }, +} satisfies PluginSchema; diff --git a/packages/better-auth/src/plugins/anonymous/anon.test.ts b/packages/better-auth/src/plugins/anonymous/anon.test.ts index b64edee8..6d29888c 100644 --- a/packages/better-auth/src/plugins/anonymous/anon.test.ts +++ b/packages/better-auth/src/plugins/anonymous/anon.test.ts @@ -12,6 +12,13 @@ describe("anonymous", async () => { async onLinkAccount(data) { linkAccountFn(data); }, + schema: { + user: { + fields: { + isAnonymous: "is_anon", + }, + }, + }, }), ], }); diff --git a/packages/better-auth/src/plugins/anonymous/index.ts b/packages/better-auth/src/plugins/anonymous/index.ts index 9201b83e..b09993c3 100644 --- a/packages/better-auth/src/plugins/anonymous/index.ts +++ b/packages/better-auth/src/plugins/anonymous/index.ts @@ -1,14 +1,16 @@ -import { - APIError, - createAuthEndpoint, - getSessionFromCtx, - sessionMiddleware, -} from "../../api"; -import type { BetterAuthPlugin, Session, User } from "../../types"; +import { APIError, createAuthEndpoint, getSessionFromCtx } from "../../api"; +import type { + BetterAuthPlugin, + InferOptionSchema, + PluginSchema, + Session, + User, +} from "../../types"; import { parseSetCookieHeader, setSessionCookie } from "../../cookies"; import { z } from "zod"; import { generateId } from "../../utils/id"; import { getOrigin } from "../../utils/url"; +import { mergeSchema } from "../../db/schema"; export interface UserWithAnonymous extends User { isAnonymous: boolean; @@ -38,8 +40,23 @@ export interface AnonymousOptions { * Disable deleting the anonymous user after linking */ disableDeleteAnonymousUser?: boolean; + /** + * Custom schema for the admin plugin + */ + schema?: InferOptionSchema; } +const schema = { + user: { + fields: { + isAnonymous: { + type: "boolean", + required: false, + }, + }, + }, +} satisfies PluginSchema; + export const anonymous = (options?: AnonymousOptions) => { return { id: "anonymous", @@ -154,15 +171,6 @@ export const anonymous = (options?: AnonymousOptions) => { }, ], }, - schema: { - user: { - fields: { - isAnonymous: { - type: "boolean", - required: false, - }, - }, - }, - }, + schema: mergeSchema(schema, options?.schema), } satisfies BetterAuthPlugin; }; diff --git a/packages/better-auth/src/plugins/jwt/index.ts b/packages/better-auth/src/plugins/jwt/index.ts index cb2776e6..2609766f 100644 --- a/packages/better-auth/src/plugins/jwt/index.ts +++ b/packages/better-auth/src/plugins/jwt/index.ts @@ -1,9 +1,10 @@ -import type { BetterAuthPlugin, User } from "../../types"; +import type { BetterAuthPlugin, InferOptionSchema, User } from "../../types"; import { type Jwk, schema } from "./schema"; import { getJwksAdapter } from "./adapter"; import { exportJWK, generateKeyPair, importJWK, SignJWT } from "jose"; import { createAuthEndpoint, sessionMiddleware } from "../../api"; import { symmetricDecrypt, symmetricEncrypt } from "../../crypto"; +import { mergeSchema } from "../../db/schema"; type JWKOptions = | { @@ -83,6 +84,10 @@ export interface JwtOptions { user: User, ) => Promise> | Record; }; + /** + * Custom schema for the admin plugin + */ + schema?: InferOptionSchema; } export const jwt = (options?: JwtOptions) => { @@ -189,6 +194,6 @@ export const jwt = (options?: JwtOptions) => { }, ), }, - schema, + schema: mergeSchema(schema, options?.schema), } satisfies BetterAuthPlugin; }; diff --git a/packages/better-auth/src/plugins/jwt/schema.ts b/packages/better-auth/src/plugins/jwt/schema.ts index 79c81b2b..ed36fbd5 100644 --- a/packages/better-auth/src/plugins/jwt/schema.ts +++ b/packages/better-auth/src/plugins/jwt/schema.ts @@ -1,7 +1,7 @@ import type { PluginSchema } from "../../types"; import { z } from "zod"; -export const schema: PluginSchema = { +export const schema = { jwks: { fields: { publicKey: { @@ -18,7 +18,7 @@ export const schema: PluginSchema = { }, }, }, -}; +} satisfies PluginSchema; export const jwk = z.object({ id: z.string(), diff --git a/packages/better-auth/src/plugins/organization/adapter.ts b/packages/better-auth/src/plugins/organization/adapter.ts index 885d0520..d426c82f 100644 --- a/packages/better-auth/src/plugins/organization/adapter.ts +++ b/packages/better-auth/src/plugins/organization/adapter.ts @@ -71,7 +71,7 @@ export const getOrgAdapter = ( organizationId: string; }) => { const user = await adapter.findOne({ - model: context.tables.user.tableName, + model: context.tables.user.modelName, where: [ { field: "email", @@ -127,7 +127,7 @@ export const getOrgAdapter = ( ], }), await adapter.findOne({ - model: context.tables.user.tableName, + model: context.tables.user.modelName, where: [ { field: "id", @@ -163,7 +163,7 @@ export const getOrgAdapter = ( return null; } const user = await adapter.findOne({ - model: context.tables.user.tableName, + model: context.tables.user.modelName, where: [ { field: "id", @@ -269,7 +269,7 @@ export const getOrgAdapter = ( organizationId: string | null, ) => { const session = await adapter.update({ - model: context.tables.session.tableName, + model: context.tables.session.modelName, where: [ { field: "id", @@ -317,7 +317,7 @@ export const getOrgAdapter = ( const userIds = members.map((member) => member.userId); const users = await adapter.findMany({ - model: context.tables.user.tableName, + model: context.tables.user.modelName, where: [{ field: "id", value: userIds, operator: "in" }], }); diff --git a/packages/better-auth/src/plugins/passkey/index.ts b/packages/better-auth/src/plugins/passkey/index.ts index 35cfce60..0eac3d39 100644 --- a/packages/better-auth/src/plugins/passkey/index.ts +++ b/packages/better-auth/src/plugins/passkey/index.ts @@ -16,11 +16,16 @@ import { z } from "zod"; import { createAuthEndpoint } from "../../api/call"; import { sessionMiddleware } from "../../api"; import { getSessionFromCtx } from "../../api/routes"; -import type { BetterAuthPlugin } from "../../types/plugins"; +import type { + BetterAuthPlugin, + InferOptionSchema, + PluginSchema, +} from "../../types/plugins"; import { setSessionCookie } from "../../cookies"; import { BetterAuthError } from "../../error"; import { generateId } from "../../utils/id"; import { env } from "../../utils/env"; +import { mergeSchema } from "../../db/schema"; interface WebAuthnChallengeValue { expectedChallenge: string; @@ -58,6 +63,10 @@ export interface PasskeyOptions { advanced?: { webAuthnChallengeCookie?: string; }; + /** + * Schema for the passkey model + */ + schema?: InferOptionSchema; } export type Passkey = { @@ -498,54 +507,56 @@ export const passkey = (options?: PasskeyOptions) => { }, ), }, - schema: { - passkey: { - fields: { - name: { - type: "string", - required: false, - }, - publicKey: { - type: "string", - required: true, - }, - userId: { - type: "string", - references: { - model: "user", - field: "id", - }, - required: true, - }, - webauthnUserID: { - type: "string", - required: true, - }, - counter: { - type: "number", - required: true, - }, - deviceType: { - type: "string", - required: true, - }, - backedUp: { - type: "boolean", - required: true, - }, - transports: { - type: "string", - required: false, - }, - createdAt: { - type: "date", - defaultValue: new Date(), - required: false, - }, - }, - }, - }, + schema: mergeSchema(schema, options?.schema), } satisfies BetterAuthPlugin; }; +const schema = { + passkey: { + fields: { + name: { + type: "string", + required: false, + }, + publicKey: { + type: "string", + required: true, + }, + userId: { + type: "string", + references: { + model: "user", + field: "id", + }, + required: true, + }, + webauthnUserID: { + type: "string", + required: true, + }, + counter: { + type: "number", + required: true, + }, + deviceType: { + type: "string", + required: true, + }, + backedUp: { + type: "boolean", + required: true, + }, + transports: { + type: "string", + required: false, + }, + createdAt: { + type: "date", + defaultValue: new Date(), + required: false, + }, + }, + }, +} satisfies PluginSchema; + export * from "./client"; diff --git a/packages/better-auth/src/plugins/phone-number/index.ts b/packages/better-auth/src/plugins/phone-number/index.ts index 10e0c488..c5063bc1 100644 --- a/packages/better-auth/src/plugins/phone-number/index.ts +++ b/packages/better-auth/src/plugins/phone-number/index.ts @@ -1,8 +1,12 @@ import { z } from "zod"; import { createAuthEndpoint } from "../../api/call"; -import type { BetterAuthPlugin } from "../../types/plugins"; +import type { + BetterAuthPlugin, + InferOptionSchema, + PluginSchema, +} from "../../types/plugins"; import { APIError } from "better-call"; -import type { User } from "../../db/schema"; +import { mergeSchema, type User } from "../../db/schema"; import { alphabet, generateRandomString } from "../../crypto/random"; import { getSessionFromCtx } from "../../api"; import { getDate } from "../../utils/date"; @@ -83,6 +87,10 @@ export const phoneNumber = (options?: { */ getTempName?: (phoneNumber: string) => string; }; + /** + * Custom schema for the admin plugin + */ + schema?: InferOptionSchema; }) => { const opts = { phoneNumber: "phoneNumber", @@ -204,7 +212,7 @@ export const phoneNumber = (options?: { } let user = await ctx.context.adapter.findOne({ - model: ctx.context.tables.user.tableName, + model: ctx.context.tables.user.modelName, where: [ { value: ctx.body.phoneNumber, @@ -280,23 +288,25 @@ export const phoneNumber = (options?: { }, ), }, - schema: { - user: { - fields: { - phoneNumber: { - type: "string", - required: false, - unique: true, - returned: true, - }, - phoneNumberVerified: { - type: "boolean", - required: false, - returned: true, - input: false, - }, - }, - }, - }, + schema: mergeSchema(schema, options?.schema), } satisfies BetterAuthPlugin; }; + +const schema = { + user: { + fields: { + phoneNumber: { + type: "string", + required: false, + unique: true, + returned: true, + }, + phoneNumberVerified: { + type: "boolean", + required: false, + returned: true, + input: false, + }, + }, + }, +} satisfies PluginSchema; diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index bc6ad77b..95193617 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -8,17 +8,18 @@ import { backupCode2fa, generateBackupCodes } from "./backup-codes"; import { otp2fa } from "./otp"; import { totp2fa } from "./totp"; import type { TwoFactorOptions, UserWithTwoFactor } from "./types"; -import type { Session } from "../../db/schema"; +import { mergeSchema, type Session } from "../../db/schema"; import { TWO_FACTOR_COOKIE_NAME, TRUST_DEVICE_COOKIE_NAME } from "./constant"; import { validatePassword } from "../../utils/password"; import { APIError } from "better-call"; import { createTOTPKeyURI } from "oslo/otp"; import { TimeSpan } from "oslo"; import { deleteSessionCookie, setSessionCookie } from "../../cookies"; +import { schema } from "./schema"; export const twoFactor = (options?: TwoFactorOptions) => { const opts = { - twoFactorTable: options?.twoFactorTable || ("twoFactor" as const), + twoFactorTable: "twoFactor", }; const totp = totp2fa( { @@ -267,42 +268,7 @@ export const twoFactor = (options?: TwoFactorOptions) => { }, ], }, - schema: { - user: { - fields: { - twoFactorEnabled: { - type: "boolean", - required: false, - defaultValue: false, - input: false, - }, - }, - }, - twoFactor: { - tableName: opts.twoFactorTable, - fields: { - secret: { - type: "string", - required: true, - returned: false, - }, - backupCodes: { - type: "string", - required: true, - returned: false, - }, - userId: { - type: "string", - required: true, - returned: false, - references: { - model: "user", - field: "id", - }, - }, - }, - }, - }, + schema: mergeSchema(schema, options?.schema), rateLimit: [ { pathMatcher(path) { diff --git a/packages/better-auth/src/plugins/two-factor/schema.ts b/packages/better-auth/src/plugins/two-factor/schema.ts new file mode 100644 index 00000000..c3737201 --- /dev/null +++ b/packages/better-auth/src/plugins/two-factor/schema.ts @@ -0,0 +1,37 @@ +import type { PluginSchema } from "../../types"; + +export const schema = { + user: { + fields: { + twoFactorEnabled: { + type: "boolean", + required: false, + defaultValue: false, + input: false, + }, + }, + }, + twoFactor: { + fields: { + secret: { + type: "string", + required: true, + returned: false, + }, + backupCodes: { + type: "string", + required: true, + returned: false, + }, + userId: { + type: "string", + required: true, + returned: false, + references: { + model: "user", + field: "id", + }, + }, + }, + }, +} satisfies PluginSchema; diff --git a/packages/better-auth/src/plugins/two-factor/types.ts b/packages/better-auth/src/plugins/two-factor/types.ts index 0f36f044..d3037a3d 100644 --- a/packages/better-auth/src/plugins/two-factor/types.ts +++ b/packages/better-auth/src/plugins/two-factor/types.ts @@ -4,6 +4,8 @@ import type { LiteralString } from "../../types/helper"; import type { BackupCodeOptions } from "./backup-codes"; import type { OTPOptions } from "./otp"; import type { TOTPOptions } from "./totp"; +import type { InferOptionSchema } from "../../types"; +import type { schema } from "./schema"; export interface TwoFactorOptions { /** @@ -22,16 +24,15 @@ export interface TwoFactorOptions { * Backup code options */ backupCodeOptions?: BackupCodeOptions; - /** - * Table name for two factor authentication. - * @default "userTwoFactor" - */ - twoFactorTable?: string; /** * Skip verification on enabling two factor authentication. * @default false */ skipVerificationOnEnable?: boolean; + /** + * Custom schema for the two factor plugin + */ + schema?: InferOptionSchema; } export interface UserWithTwoFactor extends User { diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts index e4497f9a..0f62d6aa 100644 --- a/packages/better-auth/src/plugins/username/index.ts +++ b/packages/better-auth/src/plugins/username/index.ts @@ -20,7 +20,7 @@ export const username = () => { }, async (ctx) => { const user = await ctx.context.adapter.findOne({ - model: ctx.context.tables.user.tableName, + model: ctx.context.tables.user.modelName, where: [ { field: "username", @@ -36,7 +36,7 @@ export const username = () => { }); } const account = await ctx.context.adapter.findOne({ - model: ctx.context.tables.account.tableName, + model: "account", where: [ { field: diff --git a/packages/better-auth/src/types/options.ts b/packages/better-auth/src/types/options.ts index 7c4caf8d..2f98b8c2 100644 --- a/packages/better-auth/src/types/options.ts +++ b/packages/better-auth/src/types/options.ts @@ -412,7 +412,7 @@ export interface BetterAuthOptions { * * @default "rateLimit" */ - tableName?: string; + modelName?: string; /** * Custom field names for the rate limit table */ diff --git a/packages/better-auth/src/types/plugins.ts b/packages/better-auth/src/types/plugins.ts index 3402179e..0c5a13ce 100644 --- a/packages/better-auth/src/types/plugins.ts +++ b/packages/better-auth/src/types/plugins.ts @@ -12,7 +12,7 @@ export type PluginSchema = { [field in string]: FieldAttribute; }; disableMigration?: boolean; - tableName?: string; + modelName?: string; }; }; @@ -125,3 +125,17 @@ export type BetterAuthPlugin = { pathMatcher: (path: string) => boolean; }[]; }; + +export type InferOptionSchema = S extends Record< + string, + { fields: infer Fields } +> + ? { + [K in keyof S]?: { + modelName?: string; + fields: { + [P in keyof Fields]?: string; + }; + }; + } + : never; diff --git a/packages/cli/src/generators/drizzle.ts b/packages/cli/src/generators/drizzle.ts index 482a370b..59272957 100644 --- a/packages/cli/src/generators/drizzle.ts +++ b/packages/cli/src/generators/drizzle.ts @@ -19,7 +19,7 @@ export const generateDrizzleSchema: SchemaGenerator = async ({ const fileExist = existsSync(filePath); for (const table in tables) { - const tableName = tables[table].tableName; + const modelName = tables[table].modelName; const fields = tables[table].fields; function getType(name: string, type: FieldType) { if (type === "string") { @@ -45,7 +45,7 @@ export const generateDrizzleSchema: SchemaGenerator = async ({ return `timestamp('${name}')`; } } - const schema = `export const ${table} = ${databaseType}Table("${tableName}", { + const schema = `export const ${table} = ${databaseType}Table("${modelName}", { id: text("id").primaryKey(), ${Object.keys(fields) .map((field) => { diff --git a/packages/cli/src/generators/prisma.ts b/packages/cli/src/generators/prisma.ts index d7f4be93..e9d9002e 100644 --- a/packages/cli/src/generators/prisma.ts +++ b/packages/cli/src/generators/prisma.ts @@ -28,8 +28,8 @@ export const generatePrismaSchema: SchemaGenerator = async ({ const schema = produceSchema(schemaPrisma, (builder) => { for (const table in tables) { const fields = tables[table]?.fields; - const originalTable = tables[table]?.tableName; - const tableName = capitalizeFirstLetter(originalTable || ""); + const originalTable = tables[table]?.modelName; + const modelName = capitalizeFirstLetter(originalTable || ""); function getType(type: FieldType, isOptional: boolean) { if (type === "string") { return isOptional ? "String?" : "String"; @@ -51,17 +51,17 @@ export const generatePrismaSchema: SchemaGenerator = async ({ } } const prismaModel = builder.findByType("model", { - name: tableName, + name: modelName, }); if (!prismaModel) { if (provider === "mongodb") { builder - .model(tableName) + .model(modelName) .field("id", "String") .attribute("id") .attribute(`map("_id")`); } else { - builder.model(tableName).field("id", "String").attribute("id"); + builder.model(modelName).field("id", "String").attribute("id"); } } @@ -79,14 +79,14 @@ export const generatePrismaSchema: SchemaGenerator = async ({ } builder - .model(tableName) + .model(modelName) .field(field, getType(attr.type, !attr?.required)); if (attr.unique) { - builder.model(tableName).blockAttribute(`unique([${field}])`); + builder.model(modelName).blockAttribute(`unique([${field}])`); } if (attr.references) { builder - .model(tableName) + .model(modelName) .field( `${attr.references.model.toLowerCase()}`, capitalizeFirstLetter(attr.references.model), @@ -100,8 +100,8 @@ export const generatePrismaSchema: SchemaGenerator = async ({ name: "map", within: prismaModel?.properties, }); - if (originalTable !== tableName && !hasAttribute) { - builder.model(tableName).blockAttribute("map", originalTable); + if (originalTable !== modelName && !hasAttribute) { + builder.model(modelName).blockAttribute("map", originalTable); } } });