mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 12:27:44 +00:00
feat: custom table names and fields for plugins (#570)
This commit is contained in:
@@ -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`).
|
||||
</Callout>
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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> | 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.
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -83,7 +83,7 @@ const createTransform = (
|
||||
}
|
||||
|
||||
function getModelName(model: string) {
|
||||
return schema[model].tableName;
|
||||
return schema[model].modelName;
|
||||
}
|
||||
|
||||
const shouldGenerateId = config?.generateId !== false;
|
||||
|
||||
@@ -183,7 +183,7 @@ const createTransform = (options: BetterAuthOptions) => {
|
||||
return clause;
|
||||
},
|
||||
getModelName: (model: string) => {
|
||||
return schema[model].tableName;
|
||||
return schema[model].modelName;
|
||||
},
|
||||
getField,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -61,6 +61,7 @@ export const useAuthQuery = <T>(
|
||||
});
|
||||
await opts?.onError?.(context);
|
||||
},
|
||||
|
||||
async onRequest(context) {
|
||||
const currentValue = value.get();
|
||||
value.set({
|
||||
|
||||
@@ -8,6 +8,8 @@ export type FieldType =
|
||||
| "date"
|
||||
| `${"string" | "number"}[]`;
|
||||
|
||||
type Primitive = string | number | boolean | Date | null | undefined;
|
||||
|
||||
export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||
/**
|
||||
* If the field should be required on a new record.
|
||||
@@ -24,22 +26,22 @@ export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||
* @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<T>) => InferValueType<T>;
|
||||
transform?: {
|
||||
input?: (value: InferValueType<T>) => Primitive | Promise<Primitive>;
|
||||
output?: (
|
||||
value: Primitive,
|
||||
) => InferValueType<T> | Promise<InferValueType<T>>;
|
||||
};
|
||||
/**
|
||||
* Reference to another model.
|
||||
*/
|
||||
@@ -67,14 +69,12 @@ export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<string, FieldAttribute>; tableName: string }
|
||||
{ fields: Record<string, FieldAttribute>; 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",
|
||||
|
||||
@@ -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<S extends PluginSchema>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
}
|
||||
|
||||
export const admin = (options?: AdminOptions) => {
|
||||
export const admin = <O extends AdminOptions>(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;
|
||||
|
||||
@@ -12,6 +12,13 @@ describe("anonymous", async () => {
|
||||
async onLinkAccount(data) {
|
||||
linkAccountFn(data);
|
||||
},
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
isAnonymous: "is_anon",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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<string, any>> | Record<string, any>;
|
||||
};
|
||||
/**
|
||||
* Custom schema for the admin plugin
|
||||
*/
|
||||
schema?: InferOptionSchema<typeof schema>;
|
||||
}
|
||||
|
||||
export const jwt = (options?: JwtOptions) => {
|
||||
@@ -189,6 +194,6 @@ export const jwt = (options?: JwtOptions) => {
|
||||
},
|
||||
),
|
||||
},
|
||||
schema,
|
||||
schema: mergeSchema(schema, options?.schema),
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -71,7 +71,7 @@ export const getOrgAdapter = (
|
||||
organizationId: string;
|
||||
}) => {
|
||||
const user = await adapter.findOne<User>({
|
||||
model: context.tables.user.tableName,
|
||||
model: context.tables.user.modelName,
|
||||
where: [
|
||||
{
|
||||
field: "email",
|
||||
@@ -127,7 +127,7 @@ export const getOrgAdapter = (
|
||||
],
|
||||
}),
|
||||
await adapter.findOne<User>({
|
||||
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<User>({
|
||||
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<Session>({
|
||||
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<User>({
|
||||
model: context.tables.user.tableName,
|
||||
model: context.tables.user.modelName,
|
||||
where: [{ field: "id", value: userIds, operator: "in" }],
|
||||
});
|
||||
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
}) => {
|
||||
const opts = {
|
||||
phoneNumber: "phoneNumber",
|
||||
@@ -204,7 +212,7 @@ export const phoneNumber = (options?: {
|
||||
}
|
||||
|
||||
let user = await ctx.context.adapter.findOne<UserWithPhoneNumber>({
|
||||
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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
37
packages/better-auth/src/plugins/two-factor/schema.ts
Normal file
37
packages/better-auth/src/plugins/two-factor/schema.ts
Normal file
@@ -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;
|
||||
@@ -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<typeof schema>;
|
||||
}
|
||||
|
||||
export interface UserWithTwoFactor extends User {
|
||||
|
||||
@@ -20,7 +20,7 @@ export const username = () => {
|
||||
},
|
||||
async (ctx) => {
|
||||
const user = await ctx.context.adapter.findOne<User>({
|
||||
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<Account>({
|
||||
model: ctx.context.tables.account.tableName,
|
||||
model: "account",
|
||||
where: [
|
||||
{
|
||||
field:
|
||||
|
||||
@@ -412,7 +412,7 @@ export interface BetterAuthOptions {
|
||||
*
|
||||
* @default "rateLimit"
|
||||
*/
|
||||
tableName?: string;
|
||||
modelName?: string;
|
||||
/**
|
||||
* Custom field names for the rate limit table
|
||||
*/
|
||||
|
||||
@@ -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 PluginSchema> = S extends Record<
|
||||
string,
|
||||
{ fields: infer Fields }
|
||||
>
|
||||
? {
|
||||
[K in keyof S]?: {
|
||||
modelName?: string;
|
||||
fields: {
|
||||
[P in keyof Fields]?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
: never;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user