mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20:27:44 +00:00
feat: add support for typed additional fields on core tables (#123)
This commit is contained in:
@@ -62,7 +62,7 @@ export default function UserCard(props: {
|
||||
activeSessions: Session["session"][];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { data, isPending, error } = useSession(props.session);
|
||||
const { data, isPending, error } = useSession();
|
||||
const [ua, setUa] = useState<UAParser.UAParserInstance>();
|
||||
|
||||
const session = data || props.session;
|
||||
|
||||
@@ -43,14 +43,6 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
phoneNumber({
|
||||
otp: {
|
||||
sendOTP(phoneNumber, code) {
|
||||
console.log(`Sending OTP to ${phoneNumber}: ${code}`);
|
||||
},
|
||||
sendOTPonSignUp: true,
|
||||
},
|
||||
}),
|
||||
organization({
|
||||
async sendInvitationEmail(data) {
|
||||
const res = await resend.emails.send({
|
||||
|
||||
@@ -18,17 +18,10 @@ description: Apple
|
||||
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
|
||||
|
||||
```ts title="auth.ts"
|
||||
const process = {
|
||||
env: {
|
||||
APPLE_CLIENT_ID: "" as string,
|
||||
APPLE_CLIENT_SECRET: "" as string,
|
||||
}
|
||||
}
|
||||
// ---cut---
|
||||
import { betterAuth } from "better-auth"
|
||||
import { apple } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
apple({ // [!code highlight]
|
||||
clientId: process.env.APPLE_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Discord Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { discord } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
discord: { // [!code highlight]
|
||||
clientId: process.env.DISCORD_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -16,7 +16,7 @@ To enable email and password authentication, you need to set the `emailAndPasswo
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true // [!code highlight]
|
||||
} // [!code highlight]
|
||||
@@ -86,7 +86,7 @@ To enable email verification, you need to provider `sendVerificationEmail` funct
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
async sendVerificationEmail(email, url){
|
||||
@@ -111,7 +111,7 @@ to allow users to reset a password first you need to provider `sendResetPassword
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true, // [!code highlight]
|
||||
async sendResetPassword(url, user) { // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Facebook Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { facebook } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
facebook: { // [!code highlight]
|
||||
clientId: process.env.FACEBOOK_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Github Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { github } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
github: { // [!code highlight]
|
||||
clientId: process.env.GITHUB_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Google Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { google } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
google: { // [!code highlight]
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -22,7 +22,7 @@ Enabling OAuth with Microsoft Azure Entra ID (formerly Active Directory) allows
|
||||
import { betterAuth } from "better-auth"
|
||||
import { google } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
google: { // [!code highlight]
|
||||
clientId: process.env.MICROSOFT_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Spotify Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { spotify } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
|
||||
socialProviders: { // [!code highlight]
|
||||
spotify: { // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Twitch Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twitch } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: { // [!code highlight]
|
||||
twitch: { // [!code highlight]
|
||||
clientId: process.env.TWITCH_CLIENT_ID as string, // [!code highlight]
|
||||
|
||||
@@ -19,7 +19,7 @@ description: Twitter Provider
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twitter } from "better-auth/social-providers"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {// [!code highlight]
|
||||
twitter: { // [!code highlight]
|
||||
clientId: process.env.TWITTER_CLIENT_ID, // [!code highlight]
|
||||
|
||||
@@ -14,7 +14,7 @@ Better Auth uses a library called [better-call](https://github.com/bekacru/bette
|
||||
```ts title="server.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
// add your plugins here
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ Keep in mind that this does not imply that all cookies will be shared across sub
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
crossSubDomainCookies: {
|
||||
enabled: true,
|
||||
@@ -35,7 +35,7 @@ If you want to disable the CSRF cookie, you can set `disableCsrfCheck` to `true`
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
disableCsrfCheck: true
|
||||
}
|
||||
@@ -49,7 +49,7 @@ By default, cookies are secure if the server is running in production mode. You
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
advanced: {
|
||||
useSecureCookies: true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ You can pass a database connection to Better Auth by passing a supported databas
|
||||
import { betterAuth } from "better-auth"
|
||||
import Database from "better-sqlite3"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: new Database("database.sqlite")
|
||||
})
|
||||
```
|
||||
@@ -22,7 +22,7 @@ export const auth = await betterAuth({
|
||||
import { betterAuth } from "better-auth"
|
||||
import { Pool } from "pg"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: new Pool({
|
||||
connectionString: "postgres://user:password@localhost:5432/database"
|
||||
})
|
||||
@@ -34,7 +34,7 @@ export const auth = await betterAuth({
|
||||
import { betterAuth } from "better-auth"
|
||||
import { createPool } from "mysql2/promise"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: createPool({
|
||||
host: "localhost",
|
||||
user: "root",
|
||||
@@ -50,7 +50,7 @@ export const auth = await betterAuth({
|
||||
import { betterAuth } from "better-auth"
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
dialect: new LibsqlDialect({
|
||||
url: process.env.TURSO_DATABASE_URL || "",
|
||||
@@ -71,7 +71,7 @@ See <Link href="https://kysely.dev/docs/dialects" target="_blank"> Kysley Dialec
|
||||
import { betterAuth } from "better-auth"
|
||||
import { db } from "./db"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
db: db,
|
||||
type: "sqlite" // or "mysql", "postgres" or "mssql"
|
||||
@@ -95,7 +95,7 @@ import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: "sqlite"
|
||||
})
|
||||
@@ -112,7 +112,7 @@ import { betterAuth } from "better-auth";
|
||||
import { db } from "./drizzle";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite", // or "pg" or "mysql"
|
||||
})
|
||||
@@ -129,7 +129,7 @@ import { db } from "./drizzle";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { schema } from "./schema";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite", // or "pg" or "mysql"
|
||||
schema: {
|
||||
@@ -352,9 +352,52 @@ To add new tables and columns to your database, you have two options:
|
||||
|
||||
Both methods ensure your database schema stays up-to-date with your plugins' requirements.
|
||||
|
||||
## Extending the Schema
|
||||
## Extending Core Schema
|
||||
|
||||
Better Auth doesn't allow you to directly extend any of the core schema. But you can always add new columns in your table and use database hooks to add or modify data during the lifecycle of core database operations.
|
||||
Better Auth provides a type-safe way to extend the `user` and `session` schemas. You can add custom fields to your auth config, and the CLI will automatically update the database schema. These additional fields will be properly inferred in functions like `useSession`, `signUp.email`, and other endpoints that work with user or session objects.
|
||||
|
||||
To add custom fields, use the `additionalFields` property in the `user` or `session` object of your auth config. The `additionalFields` object uses field names as keys, with each value being a `FieldAttributes` object containing:
|
||||
|
||||
- `type`: The data type of the field (e.g., "string", "number", "boolean").
|
||||
- `required`: A boolean indicating if the field is mandatory.
|
||||
- `defaultValue`: The default value for the field (note: this only applies in the JavaScript layer; in the database, the field will be optional).
|
||||
|
||||
Here's an example of how to extend the user schema with a custom "role" field:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Now you can access the `role` field in your application logic.
|
||||
|
||||
```ts
|
||||
//on signup
|
||||
const res = await auth.api.signUpEmail({
|
||||
email: "test@example.com",
|
||||
password: "password",
|
||||
name: "John Doe",
|
||||
role: "admin"
|
||||
})
|
||||
|
||||
//user object
|
||||
res.user.role // > "admin"
|
||||
```
|
||||
This configuration will:
|
||||
1. Add a "role" column to the user table in your database (if not already present).
|
||||
2. Ensure that the "role" field is available in user objects returned by Better Auth functions.
|
||||
3. Set a default value of "user" for the "role" field in your application logic.
|
||||
|
||||
## Database Hooks
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ To add a plugin on the server, include it in the `plugins` array in your auth co
|
||||
```ts title="server.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
// Add your plugins here
|
||||
]
|
||||
|
||||
@@ -24,7 +24,7 @@ You can change both the `expiresIn` and `updateAge` values by passing the `sessi
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
//... other config options
|
||||
session: {
|
||||
expiresIn: 1000 * 60 * 60 * 24 * 7 // 7 days,
|
||||
|
||||
@@ -25,7 +25,7 @@ You can also infer types on the server side.
|
||||
import { betterAuth } from "better-auth"
|
||||
import Database from "better-sqlite3"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
database: new Database("database.db")
|
||||
})
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ This plugin offers two main methods to do a second factor verification:
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins" // [!code highlight]
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
// ... other config options
|
||||
plugins: [
|
||||
twoFactor({ // [!code highlight]
|
||||
@@ -186,7 +186,7 @@ Before using OTP to verify the second factor, you need to configure `sendOTP` in
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
|
||||
@@ -17,7 +17,7 @@ The Anonymous plugin allows users to have an authenticated experience without re
|
||||
import { betterAuth } from "better-auth"
|
||||
import { anonymous } from "better-auth/plugins" // [!code highlight]
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
// ... other config options
|
||||
plugins: [
|
||||
anonymous() // [!code highlight]
|
||||
@@ -90,7 +90,7 @@ const user = await client.anonymous.linkAccount({
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
anonymous({
|
||||
emailDomainName: "example.com"
|
||||
|
||||
@@ -16,7 +16,7 @@ Magic link or email link is a way to authenticate users without a password. When
|
||||
import { betterAuth } from "better-auth";
|
||||
import { magicLink } from "better-auth/plugins";
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async (data: {
|
||||
|
||||
@@ -14,7 +14,7 @@ Organizations simplifies user access and permissions management. Assign roles an
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
plugins: [ // [!code highlight]
|
||||
organization() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
@@ -93,7 +93,7 @@ By default, any user can create an organization. To restrict this, set the `allo
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
const auth = betterAuth({
|
||||
//...
|
||||
plugins: [
|
||||
organization({
|
||||
|
||||
@@ -26,7 +26,7 @@ The passkey plugin implementation is powered by [simple-web-authn](https://simpl
|
||||
import { betterAuth } from "better-auth"
|
||||
import { passkey } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
export const auth = betterAuth({
|
||||
|
||||
plugins: [ // [!code highlight]
|
||||
passkey(), // [!code highlight]
|
||||
|
||||
@@ -15,7 +15,7 @@ The phone number plugin extends the authentication system by allowing users to s
|
||||
import { betterAuth } from "better-auth"
|
||||
import { phoneNumber } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
const auth = betterAuth({
|
||||
plugins: [
|
||||
phoneNumber({ // [!code highlight]
|
||||
otp: { // [!code highlight]
|
||||
|
||||
@@ -15,7 +15,7 @@ The username plugin wraps the email and password authenticator and adds username
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
const auth = betterAuth({
|
||||
plugins: [ // [!code highlight]
|
||||
username() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
|
||||
@@ -90,7 +90,7 @@ List of all the available options for configuring Better Auth.
|
||||
a configuration object that contains the configuration for social providers.
|
||||
|
||||
```ts title="auth.ts"
|
||||
const auth = await betterAuth({
|
||||
const auth = betterAuth({
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "better-auth",
|
||||
"version": "0.3.4-beta.2",
|
||||
"version": "0.3.4-beta.4",
|
||||
"description": "The most comprehensive authentication library for TypeScript.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -86,7 +86,7 @@ export function getEndpoints<
|
||||
getCSRFToken,
|
||||
getSession: getSession<Option>(),
|
||||
signOut,
|
||||
signUpEmail,
|
||||
signUpEmail: signUpEmail<Option>(),
|
||||
signInEmail,
|
||||
forgetPassword,
|
||||
resetPassword,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getDate } from "../../utils/date";
|
||||
import { deleteSessionCookie, setSessionCookie } from "../../utils/cookies";
|
||||
import type { Session } from "../../db/schema";
|
||||
import { z } from "zod";
|
||||
import { getIp } from "../../utils/get-request-ip";
|
||||
import type {
|
||||
BetterAuthOptions,
|
||||
InferSession,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { generateState } from "../../utils/state";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
import { getSessionFromCtx } from "./session";
|
||||
import { setSessionCookie } from "../../utils/cookies";
|
||||
import type { toZod } from "../../types/to-zod";
|
||||
|
||||
export const signInOAuth = createAuthEndpoint(
|
||||
"/sign-in/social",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { alphabet, generateRandomString } from "../../crypto/random";
|
||||
import { z } from "zod";
|
||||
import { z, ZodObject, ZodOptional, ZodString } from "zod";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
import { createEmailVerificationToken } from "./verify-email";
|
||||
import { setSessionCookie } from "../../utils/cookies";
|
||||
import { APIError } from "better-call";
|
||||
import type {
|
||||
AdditionalUserFieldsInput,
|
||||
BetterAuthOptions,
|
||||
User,
|
||||
} from "../../types";
|
||||
import type { toZod } from "../../types/to-zod";
|
||||
import { parseAdditionalUserInput } from "../../db/schema";
|
||||
|
||||
export const signUpEmail = createAuthEndpoint(
|
||||
export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
createAuthEndpoint(
|
||||
"/sign-up/email",
|
||||
{
|
||||
method: "POST",
|
||||
@@ -14,13 +22,13 @@ export const signUpEmail = createAuthEndpoint(
|
||||
currentURL: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
body: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
password: z.string(),
|
||||
image: z.string().optional(),
|
||||
callbackURL: z.string().optional(),
|
||||
}),
|
||||
body: z.record(z.string(), z.any()) as unknown as ZodObject<{
|
||||
name: ZodString;
|
||||
email: ZodString;
|
||||
password: ZodString;
|
||||
callbackURL: ZodOptional<ZodString>;
|
||||
}> &
|
||||
toZod<AdditionalUserFieldsInput<O>>,
|
||||
},
|
||||
async (ctx) => {
|
||||
if (!ctx.context.options.emailAndPassword?.enabled) {
|
||||
@@ -28,8 +36,15 @@ export const signUpEmail = createAuthEndpoint(
|
||||
message: "Email and password sign up is not enabled",
|
||||
});
|
||||
}
|
||||
const { name, email, password, image } = ctx.body;
|
||||
const body = ctx.body as any as User & {
|
||||
password: string;
|
||||
callbackURL?: string;
|
||||
} & {
|
||||
[key: string]: any;
|
||||
};
|
||||
const { name, email, password, image, ...additionalFields } = body;
|
||||
const isValidEmail = z.string().email().safeParse(email);
|
||||
|
||||
if (!isValidEmail.success) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid email",
|
||||
@@ -56,6 +71,11 @@ export const signUpEmail = createAuthEndpoint(
|
||||
message: "User already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const additionalData = parseAdditionalUserInput(
|
||||
ctx.context.options,
|
||||
additionalFields as any,
|
||||
);
|
||||
const createdUser = await ctx.context.internalAdapter.createUser({
|
||||
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||
email: email.toLowerCase(),
|
||||
@@ -64,6 +84,7 @@ export const signUpEmail = createAuthEndpoint(
|
||||
emailVerified: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...additionalData,
|
||||
});
|
||||
if (!createdUser) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
@@ -99,7 +120,7 @@ export const signUpEmail = createAuthEndpoint(
|
||||
const url = `${
|
||||
ctx.context.baseURL
|
||||
}/verify-email?token=${token}&callbackURL=${
|
||||
ctx.body.callbackURL || ctx.query?.currentURL || "/"
|
||||
body.callbackURL || ctx.query?.currentURL || "/"
|
||||
}`;
|
||||
await ctx.context.options.emailAndPassword.sendVerificationEmail?.(
|
||||
createdUser.email,
|
||||
@@ -114,9 +135,9 @@ export const signUpEmail = createAuthEndpoint(
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
body: ctx.body.callbackURL
|
||||
body: body.callbackURL
|
||||
? {
|
||||
url: ctx.body.callbackURL,
|
||||
url: body.callbackURL,
|
||||
redirect: true,
|
||||
}
|
||||
: {
|
||||
@@ -126,4 +147,4 @@ export const signUpEmail = createAuthEndpoint(
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { ReadableAtom } from "nanostores";
|
||||
import type { Session } from "../db/schema";
|
||||
import { BetterFetchError } from "@better-fetch/fetch";
|
||||
import { passkeyClient, twoFactorClient } from "../plugins";
|
||||
import { createAuthClient } from "./vanilla";
|
||||
import { organizationClient } from "./plugins";
|
||||
|
||||
describe("run time proxy", async () => {
|
||||
@@ -231,8 +230,8 @@ describe("type", () => {
|
||||
id: string;
|
||||
userId: string;
|
||||
expiresAt: Date;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
@@ -242,10 +241,10 @@ describe("type", () => {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
image?: string | undefined;
|
||||
testField4: string;
|
||||
testField?: string | undefined;
|
||||
testField2?: number | undefined;
|
||||
testField4: string;
|
||||
twoFactorEnabled?: boolean | undefined;
|
||||
twoFactorEnabled: boolean | undefined;
|
||||
};
|
||||
}>();
|
||||
});
|
||||
@@ -269,7 +268,7 @@ describe("type", () => {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
image?: string | undefined;
|
||||
twoFactorEnabled?: boolean | undefined;
|
||||
twoFactorEnabled: boolean | undefined;
|
||||
}>();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../types/helper";
|
||||
import type {
|
||||
ClientOptions,
|
||||
InferAdditionalFromClient,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
} from "./types";
|
||||
@@ -29,6 +30,15 @@ export type PathToObject<
|
||||
? { [K in CamelCase<Segment>]: Fn }
|
||||
: never;
|
||||
|
||||
type InferSignUpEmailCtx<ClientOpts extends ClientOptions> = {
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
image?: string;
|
||||
callbackURL?: string;
|
||||
fetchOptions?: BetterFetchOption<any, any, any>;
|
||||
} & UnionToIntersection<InferAdditionalFromClient<ClientOpts, "user", "input">>;
|
||||
|
||||
type InferCtx<C extends Context<any, any>> = C["body"] extends Record<
|
||||
string,
|
||||
any
|
||||
@@ -86,7 +96,11 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
|
||||
? (
|
||||
...data: HasRequiredKeys<InferCtx<C>> extends true
|
||||
? [
|
||||
Prettify<InferCtx<C>>,
|
||||
Prettify<
|
||||
T["path"] extends `/sign-up/email`
|
||||
? InferSignUpEmailCtx<COpts>
|
||||
: InferCtx<C>
|
||||
>,
|
||||
BetterFetchOption<C["body"], C["query"], C["params"]>?,
|
||||
]
|
||||
: [
|
||||
@@ -101,6 +115,7 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
|
||||
>
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type InferRoutes<
|
||||
API extends Record<string, Endpoint>,
|
||||
ClientOpts extends ClientOptions,
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from "../../plugins/passkey/client";
|
||||
export * from "../../plugins/magic-link/client";
|
||||
export * from "../../plugins/phone-number/client";
|
||||
export * from "../../plugins/anonymous/client";
|
||||
export * from "../../plugins/additional-fields/client";
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
import type { Auth } from "../auth";
|
||||
import type { InferRoutes } from "./path-to-object";
|
||||
import type { Session, User } from "../types";
|
||||
import type { FieldAttribute, InferFieldOutput } from "../db";
|
||||
import type { InferFieldsInputClient, InferFieldsOutput } from "../db";
|
||||
|
||||
export type AtomListener = {
|
||||
matcher: (path: string) => boolean;
|
||||
@@ -89,8 +89,8 @@ export type InferActions<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
>
|
||||
: {};
|
||||
/**
|
||||
* signals are just used to recall a computed value. as a
|
||||
* convention they start with "_"
|
||||
* signals are just used to recall a computed value.
|
||||
* as a convention they start with "_"
|
||||
*/
|
||||
export type IsSignal<T> = T extends `_${infer _}` ? true : false;
|
||||
|
||||
@@ -100,15 +100,17 @@ export type InferPluginsFromClient<O extends ClientOptions> =
|
||||
: undefined;
|
||||
|
||||
export type InferSessionFromClient<O extends ClientOptions> = StripEmptyObjects<
|
||||
Session & UnionToIntersection<InferAdditionalFromClient<O, "session">>
|
||||
Session &
|
||||
UnionToIntersection<InferAdditionalFromClient<O, "session", "output">>
|
||||
>;
|
||||
export type InferUserFromClient<O extends ClientOptions> = StripEmptyObjects<
|
||||
User & UnionToIntersection<InferAdditionalFromClient<O, "user">>
|
||||
User & UnionToIntersection<InferAdditionalFromClient<O, "user", "output">>
|
||||
>;
|
||||
|
||||
export type InferAdditionalFromClient<
|
||||
Options extends ClientOptions,
|
||||
Key extends string,
|
||||
Format extends "input" | "output" = "output",
|
||||
> = Options["plugins"] extends Array<infer T>
|
||||
? T extends BetterAuthClientPlugin
|
||||
? T["$InferServerPlugin"] extends {
|
||||
@@ -118,24 +120,9 @@ export type InferAdditionalFromClient<
|
||||
};
|
||||
};
|
||||
}
|
||||
? Field extends Record<infer Key, FieldAttribute>
|
||||
? {
|
||||
[key in Key as Field[key]["required"] extends false
|
||||
? never
|
||||
: Field[key]["defaultValue"] extends
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| Date
|
||||
| Function
|
||||
? key
|
||||
: never]: InferFieldOutput<Field[key]>;
|
||||
} & {
|
||||
[key in Key as Field[key]["returned"] extends false
|
||||
? never
|
||||
: key]?: InferFieldOutput<Field[key]>;
|
||||
}
|
||||
: {}
|
||||
? Format extends "input"
|
||||
? InferFieldsInputClient<Field>
|
||||
: InferFieldsOutput<Field>
|
||||
: {}
|
||||
: {}
|
||||
: {};
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
import type { ZodSchema } from "zod";
|
||||
|
||||
export const createFieldAttribute = <
|
||||
T extends FieldType,
|
||||
C extends Omit<FieldAttributeConfig<T>, "type">,
|
||||
>(
|
||||
type: T,
|
||||
config?: C,
|
||||
) => {
|
||||
return {
|
||||
type,
|
||||
...config,
|
||||
} satisfies FieldAttribute<T>;
|
||||
};
|
||||
|
||||
export type FieldAttribute<T extends FieldType = FieldType> = {
|
||||
type: T;
|
||||
} & FieldAttributeConfig<T>;
|
||||
import type { BetterAuthOptions } from "../types";
|
||||
|
||||
export type FieldType =
|
||||
| "string"
|
||||
@@ -24,25 +8,6 @@ export type FieldType =
|
||||
| "date"
|
||||
| `${"string" | "number"}[]`;
|
||||
|
||||
export type InferValueType<T extends FieldType> = T extends "string"
|
||||
? string
|
||||
: T extends "number"
|
||||
? number
|
||||
: T extends "boolean"
|
||||
? boolean
|
||||
: T extends `${infer T}[]`
|
||||
? T extends "string"
|
||||
? string[]
|
||||
: number[]
|
||||
: never;
|
||||
|
||||
export type InferFieldOutput<T extends FieldAttribute> =
|
||||
T["returned"] extends false
|
||||
? never
|
||||
: T["required"] extends false
|
||||
? InferValueType<T["type"]> | undefined
|
||||
: InferValueType<T["type"]>;
|
||||
|
||||
export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||
/**
|
||||
* If the field should be required on a new record.
|
||||
@@ -100,7 +65,124 @@ export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||
validator?: ZodSchema;
|
||||
};
|
||||
|
||||
export type FieldAttribute<T extends FieldType = FieldType> = {
|
||||
type: T;
|
||||
} & FieldAttributeConfig<T>;
|
||||
|
||||
export const createFieldAttribute = <
|
||||
T extends FieldType,
|
||||
C extends Omit<FieldAttributeConfig<T>, "type">,
|
||||
>(
|
||||
type: T,
|
||||
config?: C,
|
||||
) => {
|
||||
return {
|
||||
type,
|
||||
...config,
|
||||
} satisfies FieldAttribute<T>;
|
||||
};
|
||||
|
||||
export type InferValueType<T extends FieldType> = T extends "string"
|
||||
? string
|
||||
: T extends "number"
|
||||
? number
|
||||
: T extends "boolean"
|
||||
? boolean
|
||||
: T extends `${infer T}[]`
|
||||
? T extends "string"
|
||||
? string[]
|
||||
: number[]
|
||||
: never;
|
||||
|
||||
export type InferFieldsOutput<Field> = Field extends Record<
|
||||
infer Key,
|
||||
FieldAttribute
|
||||
>
|
||||
? {
|
||||
[key in Key as Field[key]["required"] extends false
|
||||
? Field[key]["defaultValue"] extends boolean | string | number | Date
|
||||
? key
|
||||
: never
|
||||
: key]: InferFieldOutput<Field[key]>;
|
||||
} & {
|
||||
[key in Key as Field[key]["returned"] extends false
|
||||
? never
|
||||
: key]?: InferFieldOutput<Field[key]>;
|
||||
}
|
||||
: {};
|
||||
|
||||
export type InferFieldsInput<Field> = Field extends Record<
|
||||
infer Key,
|
||||
FieldAttribute
|
||||
>
|
||||
? {
|
||||
[key in Key as Field[key]["required"] extends false
|
||||
? never
|
||||
: Field[key]["defaultValue"] extends string | number | boolean | Date
|
||||
? never
|
||||
: key]: InferFieldInput<Field[key]>;
|
||||
} & {
|
||||
[key in Key]: InferFieldInput<Field[key]> | undefined;
|
||||
}
|
||||
: {};
|
||||
|
||||
/**
|
||||
* For client will add "?" on optional fields
|
||||
*/
|
||||
export type InferFieldsInputClient<Field> = Field extends Record<
|
||||
infer Key,
|
||||
FieldAttribute
|
||||
>
|
||||
? {
|
||||
[key in Key as Field[key]["required"] extends false
|
||||
? never
|
||||
: Field[key]["defaultValue"] extends string | number | boolean | Date
|
||||
? never
|
||||
: key]: InferFieldInput<Field[key]>;
|
||||
} & {
|
||||
[key in Key]?: InferFieldInput<Field[key]> | undefined;
|
||||
}
|
||||
: {};
|
||||
|
||||
type InferFieldOutput<T extends FieldAttribute> = T["returned"] extends false
|
||||
? never
|
||||
: T["required"] extends false
|
||||
? InferValueType<T["type"]> | undefined
|
||||
: InferValueType<T["type"]>;
|
||||
|
||||
type InferFieldInput<T extends FieldAttribute> = InferValueType<T["type"]>;
|
||||
|
||||
export type PluginFieldAttribute = Omit<
|
||||
FieldAttribute,
|
||||
"transform" | "defaultValue" | "hashValue"
|
||||
>;
|
||||
|
||||
export type InferFieldsFromPlugins<
|
||||
Options extends BetterAuthOptions,
|
||||
Key extends string,
|
||||
Format extends "output" | "input" = "output",
|
||||
> = Options["plugins"] extends Array<infer T>
|
||||
? T extends {
|
||||
schema: {
|
||||
[key in Key]: {
|
||||
fields: infer Field;
|
||||
};
|
||||
};
|
||||
}
|
||||
? Format extends "output"
|
||||
? InferFieldsOutput<Field>
|
||||
: InferFieldsInput<Field>
|
||||
: {}
|
||||
: {};
|
||||
|
||||
export type InferFieldsFromOptions<
|
||||
Options extends BetterAuthOptions,
|
||||
Key extends "session" | "user",
|
||||
Format extends "output" | "input" = "output",
|
||||
> = Options[Key] extends {
|
||||
additionalFields: infer Field;
|
||||
}
|
||||
? Format extends "output"
|
||||
? InferFieldsOutput<Field>
|
||||
: InferFieldsInput<Field>
|
||||
: {};
|
||||
|
||||
@@ -89,6 +89,7 @@ export const getAuthTables = (
|
||||
required: true,
|
||||
},
|
||||
...user?.fields,
|
||||
...options.user?.additionalFields,
|
||||
},
|
||||
order: 0,
|
||||
},
|
||||
@@ -117,6 +118,7 @@ export const getAuthTables = (
|
||||
required: true,
|
||||
},
|
||||
...session?.fields,
|
||||
...options.session?.additionalFields,
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ export type Account = z.infer<typeof accountSchema>;
|
||||
export type Session = z.infer<typeof sessionSchema>;
|
||||
export type Verification = z.infer<typeof verificationSchema>;
|
||||
|
||||
export function parseData<T extends Record<string, any>>(
|
||||
export function parseOutputData<T extends Record<string, any>>(
|
||||
data: T,
|
||||
schema: {
|
||||
fields: Record<string, FieldAttribute>;
|
||||
@@ -73,7 +73,10 @@ export function parseData<T extends Record<string, any>>(
|
||||
}
|
||||
|
||||
export function getAllFields(options: BetterAuthOptions, table: string) {
|
||||
let schema: Record<string, FieldAttribute> = {};
|
||||
let schema: Record<string, FieldAttribute> = {
|
||||
...(table === "user" ? options.user?.additionalFields : {}),
|
||||
...(table === "session" ? options.session?.additionalFields : {}),
|
||||
};
|
||||
for (const plugin of options.plugins || []) {
|
||||
if (plugin.schema && plugin.schema[table]) {
|
||||
schema = {
|
||||
@@ -85,17 +88,78 @@ export function getAllFields(options: BetterAuthOptions, table: string) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
export function parseUser(options: BetterAuthOptions, user: User) {
|
||||
export function parseUserOutput(options: BetterAuthOptions, user: User) {
|
||||
const schema = getAllFields(options, "user");
|
||||
return parseData(user, { fields: schema });
|
||||
return parseOutputData(user, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseAccount(options: BetterAuthOptions, account: Account) {
|
||||
export function parseAccountOutput(
|
||||
options: BetterAuthOptions,
|
||||
account: Account,
|
||||
) {
|
||||
const schema = getAllFields(options, "account");
|
||||
return parseData(account, { fields: schema });
|
||||
return parseOutputData(account, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseSession(options: BetterAuthOptions, session: Session) {
|
||||
export function parseSessionOutput(
|
||||
options: BetterAuthOptions,
|
||||
session: Session,
|
||||
) {
|
||||
const schema = getAllFields(options, "session");
|
||||
return parseData(session, { fields: schema });
|
||||
return parseOutputData(session, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseInputData<T extends Record<string, any>>(
|
||||
data: T,
|
||||
schema: {
|
||||
fields: Record<string, FieldAttribute>;
|
||||
},
|
||||
) {
|
||||
const fields = schema.fields;
|
||||
const parsedData: Record<string, any> = {};
|
||||
for (const key in fields) {
|
||||
if (key in data) {
|
||||
parsedData[key] = data[key];
|
||||
continue;
|
||||
}
|
||||
if (fields[key].defaultValue) {
|
||||
parsedData[key] = fields[key].defaultValue;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return parsedData as Partial<T>;
|
||||
}
|
||||
|
||||
export function parseUserInput(
|
||||
options: BetterAuthOptions,
|
||||
user?: Record<string, any>,
|
||||
) {
|
||||
const schema = getAllFields(options, "user");
|
||||
return parseInputData(user || {}, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseAdditionalUserInput(
|
||||
options: BetterAuthOptions,
|
||||
user?: Record<string, any>,
|
||||
) {
|
||||
const schema = {
|
||||
...options.user?.additionalFields,
|
||||
};
|
||||
return parseInputData(user || {}, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseAccountInput(
|
||||
options: BetterAuthOptions,
|
||||
account: Partial<Account>,
|
||||
) {
|
||||
const schema = getAllFields(options, "account");
|
||||
return parseInputData(account, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseSessionInput(
|
||||
options: BetterAuthOptions,
|
||||
session: Partial<Session>,
|
||||
) {
|
||||
const schema = getAllFields(options, "session");
|
||||
return parseInputData(session, { fields: schema });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type Session } from "./../../db/schema";
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { createAuthClient } from "../../client";
|
||||
import { inferAdditionalFields } from "./client";
|
||||
import { twoFactor } from "../two-factor";
|
||||
|
||||
describe("additionalFields", async () => {
|
||||
const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({
|
||||
plugins: [twoFactor()],
|
||||
user: {
|
||||
additionalFields: {
|
||||
newField: {
|
||||
type: "string",
|
||||
defaultValue: "default-value",
|
||||
},
|
||||
nonRequiredFiled: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("should extends fields", async () => {
|
||||
const { headers } = await signInWithTestUser();
|
||||
const res = await auth.api.getSession({
|
||||
headers,
|
||||
});
|
||||
expect(res?.user.newField).toBeDefined();
|
||||
expect(res?.user.nonRequiredFiled).toBeNull();
|
||||
});
|
||||
|
||||
it("should require additional fields on signUp", async () => {
|
||||
await auth.api
|
||||
.signUpEmail({
|
||||
body: {
|
||||
email: "test@test.com",
|
||||
name: "test",
|
||||
password: "test-password",
|
||||
newField: "new-field",
|
||||
nonRequiredFiled: "non-required-field",
|
||||
},
|
||||
})
|
||||
.catch((e) => {});
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
inferAdditionalFields({
|
||||
user: {
|
||||
newField: {
|
||||
type: "string",
|
||||
},
|
||||
nonRequiredFiled: {
|
||||
type: "string",
|
||||
defaultValue: "test",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: {
|
||||
customFetchImpl,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await client.signUp.email({
|
||||
email: "test3@test.com",
|
||||
name: "test3",
|
||||
password: "test-password",
|
||||
newField: "new-field",
|
||||
});
|
||||
expect(res.data?.user.newField).toBe("new-field");
|
||||
});
|
||||
|
||||
it("should work with other plugins", async () => {
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
inferAdditionalFields({
|
||||
user: {
|
||||
newField: {
|
||||
type: "string",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
twoFactor(),
|
||||
],
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: {
|
||||
customFetchImpl,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await client.signUp.email({
|
||||
email: "test4@test.com",
|
||||
name: "test4",
|
||||
password: "test-password",
|
||||
newField: "new-field",
|
||||
});
|
||||
|
||||
expect(res.data?.user.newField).toBe("new-field");
|
||||
});
|
||||
|
||||
it("should infer it on the client", async () => {
|
||||
const client = createAuthClient({
|
||||
plugins: [inferAdditionalFields<typeof auth>()],
|
||||
});
|
||||
type t = Awaited<ReturnType<typeof client.session>>["data"];
|
||||
expectTypeOf<t>().toMatchTypeOf<{
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
image?: string | undefined;
|
||||
newField: string;
|
||||
nonRequiredFiled?: string | undefined;
|
||||
};
|
||||
session: Session;
|
||||
} | null>;
|
||||
});
|
||||
|
||||
it("should infer it on the client without direct import", async () => {
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
inferAdditionalFields({
|
||||
user: {
|
||||
newField: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
type t = Awaited<ReturnType<typeof client.session>>["data"];
|
||||
expectTypeOf<t>().toMatchTypeOf<{
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
image?: string | undefined;
|
||||
newField: string;
|
||||
};
|
||||
session: Session;
|
||||
} | null>;
|
||||
});
|
||||
});
|
||||
76
packages/better-auth/src/plugins/additional-fields/client.ts
Normal file
76
packages/better-auth/src/plugins/additional-fields/client.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { NeverToUnknown } from "@prisma/client/runtime/library";
|
||||
import type { FieldAttribute } from "../../db";
|
||||
import type { BetterAuthClientPlugin, BetterAuthOptions } from "../../types";
|
||||
import type { BetterAuthPlugin } from "../../types";
|
||||
|
||||
export const inferAdditionalFields = <
|
||||
T,
|
||||
S extends {
|
||||
user?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
session?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
} = {},
|
||||
>(
|
||||
schema?: S,
|
||||
) => {
|
||||
type Opts = T extends BetterAuthOptions
|
||||
? T
|
||||
: T extends {
|
||||
options: BetterAuthOptions;
|
||||
}
|
||||
? T["options"]
|
||||
: never;
|
||||
|
||||
type Plugin = Opts extends never
|
||||
? S extends {
|
||||
user?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
session?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
}
|
||||
? {
|
||||
id: "additional-fields-client";
|
||||
schema: {
|
||||
user: {
|
||||
fields: S["user"] extends object ? S["user"] : {};
|
||||
};
|
||||
session: {
|
||||
fields: S["session"] extends object ? S["session"] : {};
|
||||
};
|
||||
};
|
||||
}
|
||||
: never
|
||||
: Opts extends BetterAuthOptions
|
||||
? {
|
||||
id: "additional-fields";
|
||||
schema: {
|
||||
user: {
|
||||
fields: Opts["user"] extends {
|
||||
additionalFields: infer U;
|
||||
}
|
||||
? U
|
||||
: {};
|
||||
};
|
||||
session: {
|
||||
fields: Opts["session"] extends {
|
||||
additionalFields: infer U;
|
||||
}
|
||||
? U
|
||||
: {};
|
||||
};
|
||||
};
|
||||
}
|
||||
: never;
|
||||
|
||||
return {
|
||||
id: "additional-fields-client",
|
||||
$InferServerPlugin: {} as Plugin extends BetterAuthPlugin
|
||||
? Plugin
|
||||
: undefined,
|
||||
} satisfies BetterAuthClientPlugin;
|
||||
};
|
||||
@@ -195,7 +195,7 @@ export const phoneNumber = (options?: {
|
||||
});
|
||||
}
|
||||
try {
|
||||
const res = await signUpEmail({
|
||||
const res = await signUpEmail()({
|
||||
...ctx,
|
||||
options: {
|
||||
...ctx.context.options,
|
||||
|
||||
@@ -118,7 +118,7 @@ export const username = () => {
|
||||
}),
|
||||
},
|
||||
async (ctx) => {
|
||||
const res = await signUpEmail({
|
||||
const res = await signUpEmail()({
|
||||
...ctx,
|
||||
_flag: "json",
|
||||
});
|
||||
|
||||
@@ -1,57 +1,32 @@
|
||||
import type { BetterAuthOptions } from ".";
|
||||
import type { Session, User } from "../db/schema";
|
||||
import type { Auth } from "../auth";
|
||||
import type { FieldAttribute, InferFieldOutput } from "../db";
|
||||
import type { InferFieldsFromOptions, InferFieldsFromPlugins } from "../db";
|
||||
import type { StripEmptyObjects, UnionToIntersection } from "./helper";
|
||||
import type { BetterAuthPlugin } from "./plugins";
|
||||
|
||||
type InferAdditional<
|
||||
Options extends BetterAuthOptions,
|
||||
Key extends string,
|
||||
> = Options["plugins"] extends Array<infer T>
|
||||
? T extends {
|
||||
schema: {
|
||||
[key in Key]: {
|
||||
fields: infer Field;
|
||||
};
|
||||
};
|
||||
}
|
||||
? Field extends Record<infer Key, FieldAttribute>
|
||||
? {
|
||||
[key in Key as Field[key]["required"] extends false
|
||||
? never
|
||||
: Field[key]["defaultValue"] extends
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| Date
|
||||
| Function
|
||||
? key
|
||||
: never]: InferFieldOutput<Field[key]>;
|
||||
} & {
|
||||
[key in Key as Field[key]["returned"] extends false
|
||||
? never
|
||||
: key]?: InferFieldOutput<Field[key]>;
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
: {};
|
||||
export type AdditionalUserFieldsInput<Options extends BetterAuthOptions> =
|
||||
InferFieldsFromOptions<Options, "user", "input">;
|
||||
|
||||
type AdditionalSessionFields<Options extends BetterAuthOptions> =
|
||||
InferAdditional<Options, "session">;
|
||||
export type AdditionalUserFieldsOutput<Options extends BetterAuthOptions> =
|
||||
InferFieldsFromPlugins<Options, "user"> &
|
||||
InferFieldsFromOptions<Options, "user">;
|
||||
|
||||
type AdditionalUserFields<Options extends BetterAuthOptions> = InferAdditional<
|
||||
Options,
|
||||
"user"
|
||||
>;
|
||||
export type AdditionalSessionFieldsInput<Options extends BetterAuthOptions> =
|
||||
InferFieldsFromPlugins<Options, "session", "input"> &
|
||||
InferFieldsFromOptions<Options, "session", "input">;
|
||||
|
||||
export type AdditionalSessionFieldsOutput<Options extends BetterAuthOptions> =
|
||||
InferFieldsFromPlugins<Options, "session"> &
|
||||
InferFieldsFromOptions<Options, "session">;
|
||||
|
||||
export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection<
|
||||
StripEmptyObjects<
|
||||
User &
|
||||
(O extends BetterAuthOptions
|
||||
? AdditionalUserFields<O>
|
||||
? AdditionalUserFieldsOutput<O>
|
||||
: O extends Auth
|
||||
? AdditionalUserFields<O["options"]>
|
||||
? AdditionalUserFieldsOutput<O["options"]>
|
||||
: {})
|
||||
>
|
||||
>;
|
||||
@@ -61,9 +36,9 @@ export type InferSession<O extends BetterAuthOptions | Auth> =
|
||||
StripEmptyObjects<
|
||||
Session &
|
||||
(O extends BetterAuthOptions
|
||||
? AdditionalSessionFields<O>
|
||||
? AdditionalSessionFieldsOutput<O>
|
||||
: O extends Auth
|
||||
? AdditionalSessionFields<O["options"]>
|
||||
? AdditionalSessionFieldsOutput<O["options"]>
|
||||
: {})
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { RateLimit } from "./models";
|
||||
import type { Adapter } from "./adapter";
|
||||
import type { BetterSqlite3Database, MysqlPool } from "./database";
|
||||
import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types";
|
||||
import type { FieldAttribute } from "../db";
|
||||
|
||||
export interface BetterAuthOptions {
|
||||
/**
|
||||
@@ -157,6 +158,12 @@ export interface BetterAuthOptions {
|
||||
*/
|
||||
modelName?: string;
|
||||
fields?: Partial<Record<keyof User, string>>;
|
||||
/**
|
||||
* Additional fields for the session
|
||||
*/
|
||||
additionalFields?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
};
|
||||
session?: {
|
||||
modelName?: string;
|
||||
@@ -174,6 +181,12 @@ export interface BetterAuthOptions {
|
||||
* @default 1 day (60 * 60 * 24)
|
||||
*/
|
||||
updateAge?: number;
|
||||
/**
|
||||
* Additional fields for the session
|
||||
*/
|
||||
additionalFields?: {
|
||||
[key: string]: FieldAttribute;
|
||||
};
|
||||
};
|
||||
account?: {
|
||||
modelName?: string;
|
||||
|
||||
46
packages/better-auth/src/types/to-zod.ts
Normal file
46
packages/better-auth/src/types/to-zod.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
//https://github.com/colinhacks/tozod/blob/master/src/index.ts
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
type isAny<T> = [any extends T ? "true" : "false"] extends ["true"]
|
||||
? true
|
||||
: false;
|
||||
type nonoptional<T> = T extends undefined ? never : T;
|
||||
type nonnullable<T> = T extends null ? never : T;
|
||||
type equals<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
|
||||
|
||||
export type toZod<T> = {
|
||||
any: never;
|
||||
optional: z.ZodUnion<[toZod<nonoptional<T>>, z.ZodUndefined]>;
|
||||
nullable: z.ZodUnion<[toZod<nonnullable<T>>, z.ZodNull]>;
|
||||
array: T extends Array<infer U> ? z.ZodArray<toZod<U>> : never;
|
||||
string: z.ZodString;
|
||||
bigint: z.ZodBigInt;
|
||||
number: z.ZodNumber;
|
||||
boolean: z.ZodBoolean;
|
||||
date: z.ZodDate;
|
||||
object: z.ZodObject<{ [k in keyof T]: toZod<T[k]> }>;
|
||||
rest: never;
|
||||
}[zodKey<T>];
|
||||
|
||||
type zodKey<T> = isAny<T> extends true
|
||||
? "any"
|
||||
: equals<T, boolean> extends true //[T] extends [booleanUtil.Type]
|
||||
? "boolean"
|
||||
: [undefined] extends [T]
|
||||
? "optional"
|
||||
: [null] extends [T]
|
||||
? "nullable"
|
||||
: T extends any[]
|
||||
? "array"
|
||||
: equals<T, string> extends true
|
||||
? "string"
|
||||
: equals<T, bigint> extends true //[T] extends [bigintUtil.Type]
|
||||
? "bigint"
|
||||
: equals<T, number> extends true //[T] extends [numberUtil.Type]
|
||||
? "number"
|
||||
: equals<T, Date> extends true //[T] extends [dateUtil.Type]
|
||||
? "date"
|
||||
: T extends { [k: string]: any } //[T] extends [structUtil.Type]
|
||||
? "object"
|
||||
: "rest";
|
||||
@@ -37,7 +37,7 @@ describe("general types", async (it) => {
|
||||
image?: string | undefined;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
twoFactorEnabled?: boolean | undefined;
|
||||
twoFactorEnabled: boolean | undefined;
|
||||
}>();
|
||||
|
||||
expectTypeOf<typeof auth.$Infer.Session.session>().toEqualTypeOf<{
|
||||
|
||||
@@ -5,6 +5,7 @@ export function generateState(
|
||||
callbackURL?: string,
|
||||
currentURL?: string,
|
||||
dontRememberMe?: boolean,
|
||||
additionalFields?: Record<string, any>,
|
||||
) {
|
||||
const code = generateStateOAuth();
|
||||
const state = JSON.stringify({
|
||||
@@ -12,6 +13,7 @@ export function generateState(
|
||||
callbackURL,
|
||||
currentURL,
|
||||
dontRememberMe,
|
||||
additionalFields,
|
||||
});
|
||||
return { state, code };
|
||||
}
|
||||
@@ -23,6 +25,7 @@ export function parseState(state: string) {
|
||||
callbackURL: z.string().optional(),
|
||||
currentURL: z.string().optional(),
|
||||
dontRememberMe: z.boolean().optional(),
|
||||
additionalFields: z.record(z.string()).optional(),
|
||||
})
|
||||
.safeParse(JSON.parse(state));
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user