mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +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"][];
|
activeSessions: Session["session"][];
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, isPending, error } = useSession(props.session);
|
const { data, isPending, error } = useSession();
|
||||||
const [ua, setUa] = useState<UAParser.UAParserInstance>();
|
const [ua, setUa] = useState<UAParser.UAParserInstance>();
|
||||||
|
|
||||||
const session = data || props.session;
|
const session = data || props.session;
|
||||||
|
|||||||
@@ -43,14 +43,6 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
phoneNumber({
|
|
||||||
otp: {
|
|
||||||
sendOTP(phoneNumber, code) {
|
|
||||||
console.log(`Sending OTP to ${phoneNumber}: ${code}`);
|
|
||||||
},
|
|
||||||
sendOTPonSignUp: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
organization({
|
organization({
|
||||||
async sendInvitationEmail(data) {
|
async sendInvitationEmail(data) {
|
||||||
const res = await resend.emails.send({
|
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.
|
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"
|
```ts title="auth.ts"
|
||||||
const process = {
|
|
||||||
env: {
|
|
||||||
APPLE_CLIENT_ID: "" as string,
|
|
||||||
APPLE_CLIENT_SECRET: "" as string,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---cut---
|
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { apple } from "better-auth/social-providers"
|
import { apple } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
apple({ // [!code highlight]
|
apple({ // [!code highlight]
|
||||||
clientId: process.env.APPLE_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { discord } from "better-auth/social-providers"
|
import { discord } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
discord: { // [!code highlight]
|
discord: { // [!code highlight]
|
||||||
clientId: process.env.DISCORD_CLIENT_ID as string, // [!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"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
emailAndPassword: { // [!code highlight]
|
emailAndPassword: { // [!code highlight]
|
||||||
enabled: true // [!code highlight]
|
enabled: true // [!code highlight]
|
||||||
} // [!code highlight]
|
} // [!code highlight]
|
||||||
@@ -86,7 +86,7 @@ To enable email verification, you need to provider `sendVerificationEmail` funct
|
|||||||
```ts title="auth.ts"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
async sendVerificationEmail(email, url){
|
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"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
emailAndPassword: { // [!code highlight]
|
emailAndPassword: { // [!code highlight]
|
||||||
enabled: true, // [!code highlight]
|
enabled: true, // [!code highlight]
|
||||||
async sendResetPassword(url, user) { // [!code highlight]
|
async sendResetPassword(url, user) { // [!code highlight]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ description: Facebook Provider
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { facebook } from "better-auth/social-providers"
|
import { facebook } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
facebook: { // [!code highlight]
|
facebook: { // [!code highlight]
|
||||||
clientId: process.env.FACEBOOK_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { github } from "better-auth/social-providers"
|
import { github } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
github: { // [!code highlight]
|
github: { // [!code highlight]
|
||||||
clientId: process.env.GITHUB_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { google } from "better-auth/social-providers"
|
import { google } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
google: { // [!code highlight]
|
google: { // [!code highlight]
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { google } from "better-auth/social-providers"
|
import { google } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
google: { // [!code highlight]
|
google: { // [!code highlight]
|
||||||
clientId: process.env.MICROSOFT_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { spotify } from "better-auth/social-providers"
|
import { spotify } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
|
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
spotify: { // [!code highlight]
|
spotify: { // [!code highlight]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ description: Twitch Provider
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { twitch } from "better-auth/social-providers"
|
import { twitch } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: { // [!code highlight]
|
socialProviders: { // [!code highlight]
|
||||||
twitch: { // [!code highlight]
|
twitch: { // [!code highlight]
|
||||||
clientId: process.env.TWITCH_CLIENT_ID as string, // [!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 { betterAuth } from "better-auth"
|
||||||
import { twitter } from "better-auth/social-providers"
|
import { twitter } from "better-auth/social-providers"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
socialProviders: {// [!code highlight]
|
socialProviders: {// [!code highlight]
|
||||||
twitter: { // [!code highlight]
|
twitter: { // [!code highlight]
|
||||||
clientId: process.env.TWITTER_CLIENT_ID, // [!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"
|
```ts title="server.ts"
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
// add your plugins here
|
// 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"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
advanced: {
|
advanced: {
|
||||||
crossSubDomainCookies: {
|
crossSubDomainCookies: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -35,7 +35,7 @@ If you want to disable the CSRF cookie, you can set `disableCsrfCheck` to `true`
|
|||||||
```ts title="auth.ts"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
advanced: {
|
advanced: {
|
||||||
disableCsrfCheck: true
|
disableCsrfCheck: true
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ By default, cookies are secure if the server is running in production mode. You
|
|||||||
```ts title="auth.ts"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
advanced: {
|
advanced: {
|
||||||
useSecureCookies: true
|
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 { betterAuth } from "better-auth"
|
||||||
import Database from "better-sqlite3"
|
import Database from "better-sqlite3"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: new Database("database.sqlite")
|
database: new Database("database.sqlite")
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -22,7 +22,7 @@ export const auth = await betterAuth({
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { Pool } from "pg"
|
import { Pool } from "pg"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: new Pool({
|
database: new Pool({
|
||||||
connectionString: "postgres://user:password@localhost:5432/database"
|
connectionString: "postgres://user:password@localhost:5432/database"
|
||||||
})
|
})
|
||||||
@@ -34,7 +34,7 @@ export const auth = await betterAuth({
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { createPool } from "mysql2/promise"
|
import { createPool } from "mysql2/promise"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: createPool({
|
database: createPool({
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
user: "root",
|
user: "root",
|
||||||
@@ -50,7 +50,7 @@ export const auth = await betterAuth({
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: {
|
database: {
|
||||||
dialect: new LibsqlDialect({
|
dialect: new LibsqlDialect({
|
||||||
url: process.env.TURSO_DATABASE_URL || "",
|
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 { betterAuth } from "better-auth"
|
||||||
import { db } from "./db"
|
import { db } from "./db"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: {
|
database: {
|
||||||
db: db,
|
db: db,
|
||||||
type: "sqlite" // or "mysql", "postgres" or "mssql"
|
type: "sqlite" // or "mysql", "postgres" or "mssql"
|
||||||
@@ -95,7 +95,7 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: prismaAdapter(prisma, {
|
database: prismaAdapter(prisma, {
|
||||||
provider: "sqlite"
|
provider: "sqlite"
|
||||||
})
|
})
|
||||||
@@ -112,7 +112,7 @@ import { betterAuth } from "better-auth";
|
|||||||
import { db } from "./drizzle";
|
import { db } from "./drizzle";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "sqlite", // or "pg" or "mysql"
|
provider: "sqlite", // or "pg" or "mysql"
|
||||||
})
|
})
|
||||||
@@ -129,7 +129,7 @@ import { db } from "./drizzle";
|
|||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { schema } from "./schema";
|
import { schema } from "./schema";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "sqlite", // or "pg" or "mysql"
|
provider: "sqlite", // or "pg" or "mysql"
|
||||||
schema: {
|
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.
|
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
|
## Database Hooks
|
||||||
|
|
||||||
@@ -398,4 +441,4 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -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"
|
```ts title="server.ts"
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
// Add your plugins here
|
// 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"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
//... other config options
|
//... other config options
|
||||||
session: {
|
session: {
|
||||||
expiresIn: 1000 * 60 * 60 * 24 * 7 // 7 days,
|
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 { betterAuth } from "better-auth"
|
||||||
import Database from "better-sqlite3"
|
import Database from "better-sqlite3"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
database: new Database("database.db")
|
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 { betterAuth } from "better-auth"
|
||||||
import { twoFactor } from "better-auth/plugins" // [!code highlight]
|
import { twoFactor } from "better-auth/plugins" // [!code highlight]
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
// ... other config options
|
// ... other config options
|
||||||
plugins: [
|
plugins: [
|
||||||
twoFactor({ // [!code highlight]
|
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 { betterAuth } from "better-auth"
|
||||||
import { twoFactor } from "better-auth/plugins"
|
import { twoFactor } from "better-auth/plugins"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
twoFactor({
|
twoFactor({
|
||||||
otpOptions: {
|
otpOptions: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ The Anonymous plugin allows users to have an authenticated experience without re
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { anonymous } from "better-auth/plugins" // [!code highlight]
|
import { anonymous } from "better-auth/plugins" // [!code highlight]
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
// ... other config options
|
// ... other config options
|
||||||
plugins: [
|
plugins: [
|
||||||
anonymous() // [!code highlight]
|
anonymous() // [!code highlight]
|
||||||
@@ -90,7 +90,7 @@ const user = await client.anonymous.linkAccount({
|
|||||||
```ts title="auth.ts"
|
```ts title="auth.ts"
|
||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
anonymous({
|
anonymous({
|
||||||
emailDomainName: "example.com"
|
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 { betterAuth } from "better-auth";
|
||||||
import { magicLink } from "better-auth/plugins";
|
import { magicLink } from "better-auth/plugins";
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
magicLink({
|
magicLink({
|
||||||
sendMagicLink: async (data: {
|
sendMagicLink: async (data: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Organizations simplifies user access and permissions management. Assign roles an
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { organization } from "better-auth/plugins"
|
import { organization } from "better-auth/plugins"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
plugins: [ // [!code highlight]
|
plugins: [ // [!code highlight]
|
||||||
organization() // [!code highlight]
|
organization() // [!code highlight]
|
||||||
] // [!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 { betterAuth } from "better-auth"
|
||||||
import { organization } from "better-auth/plugins"
|
import { organization } from "better-auth/plugins"
|
||||||
|
|
||||||
const auth = await betterAuth({
|
const auth = betterAuth({
|
||||||
//...
|
//...
|
||||||
plugins: [
|
plugins: [
|
||||||
organization({
|
organization({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ The passkey plugin implementation is powered by [simple-web-authn](https://simpl
|
|||||||
import { betterAuth } from "better-auth"
|
import { betterAuth } from "better-auth"
|
||||||
import { passkey } from "better-auth/plugins"
|
import { passkey } from "better-auth/plugins"
|
||||||
|
|
||||||
export const auth = await betterAuth({
|
export const auth = betterAuth({
|
||||||
|
|
||||||
plugins: [ // [!code highlight]
|
plugins: [ // [!code highlight]
|
||||||
passkey(), // [!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 { betterAuth } from "better-auth"
|
||||||
import { phoneNumber } from "better-auth/plugins"
|
import { phoneNumber } from "better-auth/plugins"
|
||||||
|
|
||||||
const auth = await betterAuth({
|
const auth = betterAuth({
|
||||||
plugins: [
|
plugins: [
|
||||||
phoneNumber({ // [!code highlight]
|
phoneNumber({ // [!code highlight]
|
||||||
otp: { // [!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 { betterAuth } from "better-auth"
|
||||||
import { username } from "better-auth/plugins"
|
import { username } from "better-auth/plugins"
|
||||||
|
|
||||||
const auth = await betterAuth({
|
const auth = betterAuth({
|
||||||
plugins: [ // [!code highlight]
|
plugins: [ // [!code highlight]
|
||||||
username() // [!code highlight]
|
username() // [!code highlight]
|
||||||
] // [!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.
|
a configuration object that contains the configuration for social providers.
|
||||||
|
|
||||||
```ts title="auth.ts"
|
```ts title="auth.ts"
|
||||||
const auth = await betterAuth({
|
const auth = betterAuth({
|
||||||
socialProviders: {
|
socialProviders: {
|
||||||
google: {
|
google: {
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "better-auth",
|
"name": "better-auth",
|
||||||
"version": "0.3.4-beta.2",
|
"version": "0.3.4-beta.4",
|
||||||
"description": "The most comprehensive authentication library for TypeScript.",
|
"description": "The most comprehensive authentication library for TypeScript.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function getEndpoints<
|
|||||||
getCSRFToken,
|
getCSRFToken,
|
||||||
getSession: getSession<Option>(),
|
getSession: getSession<Option>(),
|
||||||
signOut,
|
signOut,
|
||||||
signUpEmail,
|
signUpEmail: signUpEmail<Option>(),
|
||||||
signInEmail,
|
signInEmail,
|
||||||
forgetPassword,
|
forgetPassword,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getDate } from "../../utils/date";
|
|||||||
import { deleteSessionCookie, setSessionCookie } from "../../utils/cookies";
|
import { deleteSessionCookie, setSessionCookie } from "../../utils/cookies";
|
||||||
import type { Session } from "../../db/schema";
|
import type { Session } from "../../db/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getIp } from "../../utils/get-request-ip";
|
|
||||||
import type {
|
import type {
|
||||||
BetterAuthOptions,
|
BetterAuthOptions,
|
||||||
InferSession,
|
InferSession,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { generateState } from "../../utils/state";
|
|||||||
import { createAuthEndpoint } from "../call";
|
import { createAuthEndpoint } from "../call";
|
||||||
import { getSessionFromCtx } from "./session";
|
import { getSessionFromCtx } from "./session";
|
||||||
import { setSessionCookie } from "../../utils/cookies";
|
import { setSessionCookie } from "../../utils/cookies";
|
||||||
|
import type { toZod } from "../../types/to-zod";
|
||||||
|
|
||||||
export const signInOAuth = createAuthEndpoint(
|
export const signInOAuth = createAuthEndpoint(
|
||||||
"/sign-in/social",
|
"/sign-in/social",
|
||||||
|
|||||||
@@ -1,129 +1,150 @@
|
|||||||
import { alphabet, generateRandomString } from "../../crypto/random";
|
import { alphabet, generateRandomString } from "../../crypto/random";
|
||||||
import { z } from "zod";
|
import { z, ZodObject, ZodOptional, ZodString } from "zod";
|
||||||
import { createAuthEndpoint } from "../call";
|
import { createAuthEndpoint } from "../call";
|
||||||
import { createEmailVerificationToken } from "./verify-email";
|
import { createEmailVerificationToken } from "./verify-email";
|
||||||
import { setSessionCookie } from "../../utils/cookies";
|
import { setSessionCookie } from "../../utils/cookies";
|
||||||
import { APIError } from "better-call";
|
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>() =>
|
||||||
"/sign-up/email",
|
createAuthEndpoint(
|
||||||
{
|
"/sign-up/email",
|
||||||
method: "POST",
|
{
|
||||||
query: z
|
method: "POST",
|
||||||
.object({
|
query: z
|
||||||
currentURL: z.string().optional(),
|
.object({
|
||||||
})
|
currentURL: z.string().optional(),
|
||||||
.optional(),
|
})
|
||||||
body: z.object({
|
.optional(),
|
||||||
name: z.string(),
|
body: z.record(z.string(), z.any()) as unknown as ZodObject<{
|
||||||
email: z.string(),
|
name: ZodString;
|
||||||
password: z.string(),
|
email: ZodString;
|
||||||
image: z.string().optional(),
|
password: ZodString;
|
||||||
callbackURL: z.string().optional(),
|
callbackURL: ZodOptional<ZodString>;
|
||||||
}),
|
}> &
|
||||||
},
|
toZod<AdditionalUserFieldsInput<O>>,
|
||||||
async (ctx) => {
|
},
|
||||||
if (!ctx.context.options.emailAndPassword?.enabled) {
|
async (ctx) => {
|
||||||
throw new APIError("BAD_REQUEST", {
|
if (!ctx.context.options.emailAndPassword?.enabled) {
|
||||||
message: "Email and password sign up is not enabled",
|
throw new APIError("BAD_REQUEST", {
|
||||||
});
|
message: "Email and password sign up is not enabled",
|
||||||
}
|
});
|
||||||
const { name, email, password, image } = ctx.body;
|
}
|
||||||
const isValidEmail = z.string().email().safeParse(email);
|
const body = ctx.body as any as User & {
|
||||||
if (!isValidEmail.success) {
|
password: string;
|
||||||
throw new APIError("BAD_REQUEST", {
|
callbackURL?: string;
|
||||||
message: "Invalid email",
|
} & {
|
||||||
});
|
[key: string]: any;
|
||||||
}
|
};
|
||||||
|
const { name, email, password, image, ...additionalFields } = body;
|
||||||
|
const isValidEmail = z.string().email().safeParse(email);
|
||||||
|
|
||||||
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
if (!isValidEmail.success) {
|
||||||
if (password.length < minPasswordLength) {
|
throw new APIError("BAD_REQUEST", {
|
||||||
ctx.context.logger.error("Password is too short");
|
message: "Invalid email",
|
||||||
throw new APIError("BAD_REQUEST", {
|
});
|
||||||
message: "Password is too short",
|
}
|
||||||
});
|
|
||||||
}
|
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
||||||
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
if (password.length < minPasswordLength) {
|
||||||
if (password.length > maxPasswordLength) {
|
ctx.context.logger.error("Password is too short");
|
||||||
ctx.context.logger.error("Password is too long");
|
throw new APIError("BAD_REQUEST", {
|
||||||
throw new APIError("BAD_REQUEST", {
|
message: "Password is too short",
|
||||||
message: "Password is too long",
|
});
|
||||||
});
|
}
|
||||||
}
|
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
||||||
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
if (password.length > maxPasswordLength) {
|
||||||
if (dbUser?.user) {
|
ctx.context.logger.error("Password is too long");
|
||||||
throw new APIError("BAD_REQUEST", {
|
throw new APIError("BAD_REQUEST", {
|
||||||
message: "User already exists",
|
message: "Password is too long",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const createdUser = await ctx.context.internalAdapter.createUser({
|
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
||||||
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
if (dbUser?.user) {
|
||||||
email: email.toLowerCase(),
|
throw new APIError("BAD_REQUEST", {
|
||||||
name,
|
message: "User already exists",
|
||||||
image,
|
});
|
||||||
emailVerified: false,
|
}
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
const additionalData = parseAdditionalUserInput(
|
||||||
});
|
ctx.context.options,
|
||||||
if (!createdUser) {
|
additionalFields as any,
|
||||||
throw new APIError("BAD_REQUEST", {
|
|
||||||
message: "Couldn't create user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Link the account to the user
|
|
||||||
*/
|
|
||||||
const hash = await ctx.context.password.hash(password);
|
|
||||||
await ctx.context.internalAdapter.linkAccount({
|
|
||||||
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
|
||||||
userId: createdUser.id,
|
|
||||||
providerId: "credential",
|
|
||||||
accountId: createdUser.id,
|
|
||||||
password: hash,
|
|
||||||
});
|
|
||||||
const session = await ctx.context.internalAdapter.createSession(
|
|
||||||
createdUser.id,
|
|
||||||
ctx.request,
|
|
||||||
);
|
|
||||||
if (!session) {
|
|
||||||
throw new APIError("BAD_REQUEST", {
|
|
||||||
message: "Couldn't create session",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await setSessionCookie(ctx, session.id);
|
|
||||||
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
|
|
||||||
const token = await createEmailVerificationToken(
|
|
||||||
ctx.context.secret,
|
|
||||||
createdUser.email,
|
|
||||||
);
|
);
|
||||||
const url = `${
|
const createdUser = await ctx.context.internalAdapter.createUser({
|
||||||
ctx.context.baseURL
|
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||||
}/verify-email?token=${token}&callbackURL=${
|
email: email.toLowerCase(),
|
||||||
ctx.body.callbackURL || ctx.query?.currentURL || "/"
|
name,
|
||||||
}`;
|
image,
|
||||||
await ctx.context.options.emailAndPassword.sendVerificationEmail?.(
|
emailVerified: false,
|
||||||
createdUser.email,
|
createdAt: new Date(),
|
||||||
url,
|
updatedAt: new Date(),
|
||||||
token,
|
...additionalData,
|
||||||
|
});
|
||||||
|
if (!createdUser) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "Couldn't create user",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Link the account to the user
|
||||||
|
*/
|
||||||
|
const hash = await ctx.context.password.hash(password);
|
||||||
|
await ctx.context.internalAdapter.linkAccount({
|
||||||
|
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||||
|
userId: createdUser.id,
|
||||||
|
providerId: "credential",
|
||||||
|
accountId: createdUser.id,
|
||||||
|
password: hash,
|
||||||
|
});
|
||||||
|
const session = await ctx.context.internalAdapter.createSession(
|
||||||
|
createdUser.id,
|
||||||
|
ctx.request,
|
||||||
);
|
);
|
||||||
}
|
if (!session) {
|
||||||
return ctx.json(
|
throw new APIError("BAD_REQUEST", {
|
||||||
{
|
message: "Couldn't create session",
|
||||||
user: createdUser,
|
});
|
||||||
session,
|
}
|
||||||
error: null,
|
await setSessionCookie(ctx, session.id);
|
||||||
},
|
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
|
||||||
{
|
const token = await createEmailVerificationToken(
|
||||||
body: ctx.body.callbackURL
|
ctx.context.secret,
|
||||||
? {
|
createdUser.email,
|
||||||
url: ctx.body.callbackURL,
|
);
|
||||||
redirect: true,
|
const url = `${
|
||||||
}
|
ctx.context.baseURL
|
||||||
: {
|
}/verify-email?token=${token}&callbackURL=${
|
||||||
user: createdUser,
|
body.callbackURL || ctx.query?.currentURL || "/"
|
||||||
session,
|
}`;
|
||||||
},
|
await ctx.context.options.emailAndPassword.sendVerificationEmail?.(
|
||||||
},
|
createdUser.email,
|
||||||
);
|
url,
|
||||||
},
|
token,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return ctx.json(
|
||||||
|
{
|
||||||
|
user: createdUser,
|
||||||
|
session,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: body.callbackURL
|
||||||
|
? {
|
||||||
|
url: body.callbackURL,
|
||||||
|
redirect: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
user: createdUser,
|
||||||
|
session,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import type { ReadableAtom } from "nanostores";
|
|||||||
import type { Session } from "../db/schema";
|
import type { Session } from "../db/schema";
|
||||||
import { BetterFetchError } from "@better-fetch/fetch";
|
import { BetterFetchError } from "@better-fetch/fetch";
|
||||||
import { passkeyClient, twoFactorClient } from "../plugins";
|
import { passkeyClient, twoFactorClient } from "../plugins";
|
||||||
import { createAuthClient } from "./vanilla";
|
|
||||||
import { organizationClient } from "./plugins";
|
import { organizationClient } from "./plugins";
|
||||||
|
|
||||||
describe("run time proxy", async () => {
|
describe("run time proxy", async () => {
|
||||||
@@ -231,8 +230,8 @@ describe("type", () => {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
ipAddress?: string;
|
ipAddress?: string | undefined;
|
||||||
userAgent?: string;
|
userAgent?: string | undefined;
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -242,10 +241,10 @@ describe("type", () => {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
image?: string | undefined;
|
image?: string | undefined;
|
||||||
|
testField4: string;
|
||||||
testField?: string | undefined;
|
testField?: string | undefined;
|
||||||
testField2?: number | undefined;
|
testField2?: number | undefined;
|
||||||
testField4: string;
|
twoFactorEnabled: boolean | undefined;
|
||||||
twoFactorEnabled?: boolean | undefined;
|
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
});
|
});
|
||||||
@@ -269,7 +268,7 @@ describe("type", () => {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
image?: string | undefined;
|
image?: string | undefined;
|
||||||
twoFactorEnabled?: boolean | undefined;
|
twoFactorEnabled: boolean | undefined;
|
||||||
}>();
|
}>();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
} from "../types/helper";
|
} from "../types/helper";
|
||||||
import type {
|
import type {
|
||||||
ClientOptions,
|
ClientOptions,
|
||||||
|
InferAdditionalFromClient,
|
||||||
InferSessionFromClient,
|
InferSessionFromClient,
|
||||||
InferUserFromClient,
|
InferUserFromClient,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -29,6 +30,15 @@ export type PathToObject<
|
|||||||
? { [K in CamelCase<Segment>]: Fn }
|
? { [K in CamelCase<Segment>]: Fn }
|
||||||
: never;
|
: 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<
|
type InferCtx<C extends Context<any, any>> = C["body"] extends Record<
|
||||||
string,
|
string,
|
||||||
any
|
any
|
||||||
@@ -86,7 +96,11 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
|
|||||||
? (
|
? (
|
||||||
...data: HasRequiredKeys<InferCtx<C>> extends true
|
...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"]>?,
|
BetterFetchOption<C["body"], C["query"], C["params"]>?,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -101,6 +115,7 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
|
|||||||
>
|
>
|
||||||
: never
|
: never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type InferRoutes<
|
export type InferRoutes<
|
||||||
API extends Record<string, Endpoint>,
|
API extends Record<string, Endpoint>,
|
||||||
ClientOpts extends ClientOptions,
|
ClientOpts extends ClientOptions,
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from "../../plugins/passkey/client";
|
|||||||
export * from "../../plugins/magic-link/client";
|
export * from "../../plugins/magic-link/client";
|
||||||
export * from "../../plugins/phone-number/client";
|
export * from "../../plugins/phone-number/client";
|
||||||
export * from "../../plugins/anonymous/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 { Auth } from "../auth";
|
||||||
import type { InferRoutes } from "./path-to-object";
|
import type { InferRoutes } from "./path-to-object";
|
||||||
import type { Session, User } from "../types";
|
import type { Session, User } from "../types";
|
||||||
import type { FieldAttribute, InferFieldOutput } from "../db";
|
import type { InferFieldsInputClient, InferFieldsOutput } from "../db";
|
||||||
|
|
||||||
export type AtomListener = {
|
export type AtomListener = {
|
||||||
matcher: (path: string) => boolean;
|
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
|
* signals are just used to recall a computed value.
|
||||||
* convention they start with "_"
|
* as a convention they start with "_"
|
||||||
*/
|
*/
|
||||||
export type IsSignal<T> = T extends `_${infer _}` ? true : false;
|
export type IsSignal<T> = T extends `_${infer _}` ? true : false;
|
||||||
|
|
||||||
@@ -100,15 +100,17 @@ export type InferPluginsFromClient<O extends ClientOptions> =
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
export type InferSessionFromClient<O extends ClientOptions> = StripEmptyObjects<
|
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<
|
export type InferUserFromClient<O extends ClientOptions> = StripEmptyObjects<
|
||||||
User & UnionToIntersection<InferAdditionalFromClient<O, "user">>
|
User & UnionToIntersection<InferAdditionalFromClient<O, "user", "output">>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type InferAdditionalFromClient<
|
export type InferAdditionalFromClient<
|
||||||
Options extends ClientOptions,
|
Options extends ClientOptions,
|
||||||
Key extends string,
|
Key extends string,
|
||||||
|
Format extends "input" | "output" = "output",
|
||||||
> = Options["plugins"] extends Array<infer T>
|
> = Options["plugins"] extends Array<infer T>
|
||||||
? T extends BetterAuthClientPlugin
|
? T extends BetterAuthClientPlugin
|
||||||
? T["$InferServerPlugin"] extends {
|
? T["$InferServerPlugin"] extends {
|
||||||
@@ -118,24 +120,9 @@ export type InferAdditionalFromClient<
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
? Field extends Record<infer Key, FieldAttribute>
|
? Format extends "input"
|
||||||
? {
|
? InferFieldsInputClient<Field>
|
||||||
[key in Key as Field[key]["required"] extends false
|
: InferFieldsOutput<Field>
|
||||||
? 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]>;
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
: {}
|
: {}
|
||||||
: {}
|
: {}
|
||||||
: {};
|
: {};
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
import type { ZodSchema } from "zod";
|
import type { ZodSchema } from "zod";
|
||||||
|
import type { BetterAuthOptions } from "../types";
|
||||||
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>;
|
|
||||||
|
|
||||||
export type FieldType =
|
export type FieldType =
|
||||||
| "string"
|
| "string"
|
||||||
@@ -24,25 +8,6 @@ export type FieldType =
|
|||||||
| "date"
|
| "date"
|
||||||
| `${"string" | "number"}[]`;
|
| `${"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> = {
|
export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
||||||
/**
|
/**
|
||||||
* If the field should be required on a new record.
|
* If the field should be required on a new record.
|
||||||
@@ -100,7 +65,124 @@ export type FieldAttributeConfig<T extends FieldType = FieldType> = {
|
|||||||
validator?: ZodSchema;
|
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<
|
export type PluginFieldAttribute = Omit<
|
||||||
FieldAttribute,
|
FieldAttribute,
|
||||||
"transform" | "defaultValue" | "hashValue"
|
"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,
|
required: true,
|
||||||
},
|
},
|
||||||
...user?.fields,
|
...user?.fields,
|
||||||
|
...options.user?.additionalFields,
|
||||||
},
|
},
|
||||||
order: 0,
|
order: 0,
|
||||||
},
|
},
|
||||||
@@ -117,6 +118,7 @@ export const getAuthTables = (
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
...session?.fields,
|
...session?.fields,
|
||||||
|
...options.session?.additionalFields,
|
||||||
},
|
},
|
||||||
order: 1,
|
order: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export type Account = z.infer<typeof accountSchema>;
|
|||||||
export type Session = z.infer<typeof sessionSchema>;
|
export type Session = z.infer<typeof sessionSchema>;
|
||||||
export type Verification = z.infer<typeof verificationSchema>;
|
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,
|
data: T,
|
||||||
schema: {
|
schema: {
|
||||||
fields: Record<string, FieldAttribute>;
|
fields: Record<string, FieldAttribute>;
|
||||||
@@ -73,7 +73,10 @@ export function parseData<T extends Record<string, any>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAllFields(options: BetterAuthOptions, table: string) {
|
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 || []) {
|
for (const plugin of options.plugins || []) {
|
||||||
if (plugin.schema && plugin.schema[table]) {
|
if (plugin.schema && plugin.schema[table]) {
|
||||||
schema = {
|
schema = {
|
||||||
@@ -85,17 +88,78 @@ export function getAllFields(options: BetterAuthOptions, table: string) {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUser(options: BetterAuthOptions, user: User) {
|
export function parseUserOutput(options: BetterAuthOptions, user: User) {
|
||||||
const schema = getAllFields(options, "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");
|
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");
|
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 {
|
try {
|
||||||
const res = await signUpEmail({
|
const res = await signUpEmail()({
|
||||||
...ctx,
|
...ctx,
|
||||||
options: {
|
options: {
|
||||||
...ctx.context.options,
|
...ctx.context.options,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const username = () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const res = await signUpEmail({
|
const res = await signUpEmail()({
|
||||||
...ctx,
|
...ctx,
|
||||||
_flag: "json",
|
_flag: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,57 +1,32 @@
|
|||||||
import type { BetterAuthOptions } from ".";
|
import type { BetterAuthOptions } from ".";
|
||||||
import type { Session, User } from "../db/schema";
|
import type { Session, User } from "../db/schema";
|
||||||
import type { Auth } from "../auth";
|
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 { StripEmptyObjects, UnionToIntersection } from "./helper";
|
||||||
import type { BetterAuthPlugin } from "./plugins";
|
import type { BetterAuthPlugin } from "./plugins";
|
||||||
|
|
||||||
type InferAdditional<
|
export type AdditionalUserFieldsInput<Options extends BetterAuthOptions> =
|
||||||
Options extends BetterAuthOptions,
|
InferFieldsFromOptions<Options, "user", "input">;
|
||||||
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]>;
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
: {}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
type AdditionalSessionFields<Options extends BetterAuthOptions> =
|
export type AdditionalUserFieldsOutput<Options extends BetterAuthOptions> =
|
||||||
InferAdditional<Options, "session">;
|
InferFieldsFromPlugins<Options, "user"> &
|
||||||
|
InferFieldsFromOptions<Options, "user">;
|
||||||
|
|
||||||
type AdditionalUserFields<Options extends BetterAuthOptions> = InferAdditional<
|
export type AdditionalSessionFieldsInput<Options extends BetterAuthOptions> =
|
||||||
Options,
|
InferFieldsFromPlugins<Options, "session", "input"> &
|
||||||
"user"
|
InferFieldsFromOptions<Options, "session", "input">;
|
||||||
>;
|
|
||||||
|
export type AdditionalSessionFieldsOutput<Options extends BetterAuthOptions> =
|
||||||
|
InferFieldsFromPlugins<Options, "session"> &
|
||||||
|
InferFieldsFromOptions<Options, "session">;
|
||||||
|
|
||||||
export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection<
|
export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection<
|
||||||
StripEmptyObjects<
|
StripEmptyObjects<
|
||||||
User &
|
User &
|
||||||
(O extends BetterAuthOptions
|
(O extends BetterAuthOptions
|
||||||
? AdditionalUserFields<O>
|
? AdditionalUserFieldsOutput<O>
|
||||||
: O extends Auth
|
: O extends Auth
|
||||||
? AdditionalUserFields<O["options"]>
|
? AdditionalUserFieldsOutput<O["options"]>
|
||||||
: {})
|
: {})
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
@@ -61,9 +36,9 @@ export type InferSession<O extends BetterAuthOptions | Auth> =
|
|||||||
StripEmptyObjects<
|
StripEmptyObjects<
|
||||||
Session &
|
Session &
|
||||||
(O extends BetterAuthOptions
|
(O extends BetterAuthOptions
|
||||||
? AdditionalSessionFields<O>
|
? AdditionalSessionFieldsOutput<O>
|
||||||
: O extends Auth
|
: 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 { Adapter } from "./adapter";
|
||||||
import type { BetterSqlite3Database, MysqlPool } from "./database";
|
import type { BetterSqlite3Database, MysqlPool } from "./database";
|
||||||
import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types";
|
import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types";
|
||||||
|
import type { FieldAttribute } from "../db";
|
||||||
|
|
||||||
export interface BetterAuthOptions {
|
export interface BetterAuthOptions {
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +158,12 @@ export interface BetterAuthOptions {
|
|||||||
*/
|
*/
|
||||||
modelName?: string;
|
modelName?: string;
|
||||||
fields?: Partial<Record<keyof User, string>>;
|
fields?: Partial<Record<keyof User, string>>;
|
||||||
|
/**
|
||||||
|
* Additional fields for the session
|
||||||
|
*/
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: FieldAttribute;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
session?: {
|
session?: {
|
||||||
modelName?: string;
|
modelName?: string;
|
||||||
@@ -174,6 +181,12 @@ export interface BetterAuthOptions {
|
|||||||
* @default 1 day (60 * 60 * 24)
|
* @default 1 day (60 * 60 * 24)
|
||||||
*/
|
*/
|
||||||
updateAge?: number;
|
updateAge?: number;
|
||||||
|
/**
|
||||||
|
* Additional fields for the session
|
||||||
|
*/
|
||||||
|
additionalFields?: {
|
||||||
|
[key: string]: FieldAttribute;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
account?: {
|
account?: {
|
||||||
modelName?: string;
|
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;
|
image?: string | undefined;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
twoFactorEnabled?: boolean | undefined;
|
twoFactorEnabled: boolean | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
expectTypeOf<typeof auth.$Infer.Session.session>().toEqualTypeOf<{
|
expectTypeOf<typeof auth.$Infer.Session.session>().toEqualTypeOf<{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export function generateState(
|
|||||||
callbackURL?: string,
|
callbackURL?: string,
|
||||||
currentURL?: string,
|
currentURL?: string,
|
||||||
dontRememberMe?: boolean,
|
dontRememberMe?: boolean,
|
||||||
|
additionalFields?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
const code = generateStateOAuth();
|
const code = generateStateOAuth();
|
||||||
const state = JSON.stringify({
|
const state = JSON.stringify({
|
||||||
@@ -12,6 +13,7 @@ export function generateState(
|
|||||||
callbackURL,
|
callbackURL,
|
||||||
currentURL,
|
currentURL,
|
||||||
dontRememberMe,
|
dontRememberMe,
|
||||||
|
additionalFields,
|
||||||
});
|
});
|
||||||
return { state, code };
|
return { state, code };
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@ export function parseState(state: string) {
|
|||||||
callbackURL: z.string().optional(),
|
callbackURL: z.string().optional(),
|
||||||
currentURL: z.string().optional(),
|
currentURL: z.string().optional(),
|
||||||
dontRememberMe: z.boolean().optional(),
|
dontRememberMe: z.boolean().optional(),
|
||||||
|
additionalFields: z.record(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.safeParse(JSON.parse(state));
|
.safeParse(JSON.parse(state));
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user