admin: Superforms + Zod validation, three-address-defaults schema, profiles toast fix

- Add Superforms + Zod 4 for all admin form validation
- Replace address isDefault with three separate boolean defaults (Mailing, Billing, Shipping)
- Update profiles page with three-checkbox address form and toast improvements
- Convert all admin page server actions to pass request directly to crud
- Export superFormErrorsToFieldErrors helper for profiles server actions
- Regenerate Prisma client for new schema fields
This commit is contained in:
NeilClack
2026-07-07 13:36:23 -05:00
parent 38347f5f24
commit 38d1752f5b
98 changed files with 3408 additions and 1758 deletions
@@ -11,33 +11,33 @@ description: Configure TOTP authenticator apps, send OTP codes via email/SMS, ma
4. Verify: check that `twoFactorSecret` column exists on user table
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
export const auth = betterAuth({
appName: "My App",
plugins: [
twoFactor({
issuer: "My App",
}),
],
appName: 'My App',
plugins: [
twoFactor({
issuer: 'My App'
})
]
});
```
### Client-Side Setup
```ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from 'better-auth/client';
import { twoFactorClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
],
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = '/2fa';
}
})
]
});
```
@@ -47,14 +47,14 @@ Requires password verification. Returns TOTP URI (for QR code) and backup codes.
```ts
const enable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({
password,
});
const { data, error } = await authClient.twoFactor.enable({
password
});
if (data) {
// data.totpURI — generate a QR code from this
// data.backupCodes — display to user
}
if (data) {
// data.totpURI — generate a QR code from this
// data.backupCodes — display to user
}
};
```
@@ -65,10 +65,10 @@ const enable2FA = async (password: string) => {
### Displaying the QR Code
```tsx
import QRCode from "react-qr-code";
import QRCode from 'react-qr-code';
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
return <QRCode value={totpURI} />;
return <QRCode value={totpURI} />;
};
```
@@ -78,10 +78,10 @@ Accepts codes from one period before/after current time:
```ts
const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true,
});
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true
});
};
```
@@ -89,10 +89,10 @@ const verifyTotp = async (code: string) => {
```ts
twoFactor({
totpOptions: {
digits: 6, // 6 or 8 digits (default: 6)
period: 30, // Code validity period in seconds (default: 30)
},
totpOptions: {
digits: 6, // 6 or 8 digits (default: 6)
period: 30 // Code validity period in seconds (default: 30)
}
});
```
@@ -101,27 +101,27 @@ twoFactor({
### Configuring OTP Delivery
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
plugins: [
twoFactor({
otpOptions: {
sendOTP: async ({ user, otp }, ctx) => {
await sendEmail({
to: user.email,
subject: "Your verification code",
text: `Your code is: ${otp}`,
});
},
period: 5, // Code validity in minutes (default: 3)
digits: 6, // Number of digits (default: 6)
allowedAttempts: 5, // Max verification attempts (default: 5)
},
}),
],
plugins: [
twoFactor({
otpOptions: {
sendOTP: async ({ user, otp }, ctx) => {
await sendEmail({
to: user.email,
subject: 'Your verification code',
text: `Your code is: ${otp}`
});
},
period: 5, // Code validity in minutes (default: 3)
digits: 6, // Number of digits (default: 6)
allowedAttempts: 5 // Max verification attempts (default: 5)
}
})
]
});
```
@@ -135,9 +135,9 @@ Configure how OTP codes are stored in the database:
```ts
twoFactor({
otpOptions: {
storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
},
otpOptions: {
storeOTP: 'encrypted' // Options: "plain", "encrypted", "hashed"
}
});
```
@@ -145,12 +145,12 @@ For custom encryption:
```ts
twoFactor({
otpOptions: {
storeOTP: {
encrypt: async (token) => myEncrypt(token),
decrypt: async (token) => myDecrypt(token),
},
},
otpOptions: {
storeOTP: {
encrypt: async (token) => myEncrypt(token),
decrypt: async (token) => myDecrypt(token)
}
}
});
```
@@ -162,16 +162,16 @@ Generated automatically when 2FA is enabled. Each code is single-use.
```tsx
const BackupCodes = ({ codes }: { codes: string[] }) => {
return (
<div>
<p>Save these codes in a secure location:</p>
<ul>
{codes.map((code, i) => (
<li key={i}>{code}</li>
))}
</ul>
</div>
);
return (
<div>
<p>Save these codes in a secure location:</p>
<ul>
{codes.map((code, i) => (
<li key={i}>{code}</li>
))}
</ul>
</div>
);
};
```
@@ -181,10 +181,10 @@ Invalidates all previous codes:
```ts
const regenerateBackupCodes = async (password: string) => {
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password,
});
// data.backupCodes contains the new codes
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password
});
// data.backupCodes contains the new codes
};
```
@@ -192,10 +192,10 @@ const regenerateBackupCodes = async (password: string) => {
```ts
const verifyBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code,
trustDevice: true,
});
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code,
trustDevice: true
});
};
```
@@ -203,11 +203,11 @@ const verifyBackupCode = async (code: string) => {
```ts
twoFactor({
backupCodeOptions: {
amount: 10, // Number of codes to generate (default: 10)
length: 10, // Length of each code (default: 10)
storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
},
backupCodeOptions: {
amount: 10, // Number of codes to generate (default: 10)
length: 10, // Length of each code (default: 10)
storeBackupCodes: 'encrypted' // Options: "plain", "encrypted"
}
});
```
@@ -225,16 +225,16 @@ Response includes `twoFactorRedirect: true` when 2FA is required:
```ts
const signIn = async (email: string, password: string) => {
const { data, error } = await authClient.signIn.email(
{ email, password },
{
onSuccess(context) {
if (context.data.twoFactorRedirect) {
window.location.href = "/2fa";
}
},
}
);
const { data, error } = await authClient.signIn.email(
{ email, password },
{
onSuccess(context) {
if (context.data.twoFactorRedirect) {
window.location.href = '/2fa';
}
}
}
);
};
```
@@ -252,7 +252,7 @@ Flow: credentials → session removed → temporary 2FA cookie (10 min default)
```ts
twoFactor({
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
twoFactorCookieMaxAge: 600 // 10 minutes in seconds (default)
});
```
@@ -262,9 +262,9 @@ Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional at
```ts
twoFactor({
otpOptions: {
allowedAttempts: 5, // Max attempts per OTP code (default: 5)
},
otpOptions: {
allowedAttempts: 5 // Max attempts per OTP code (default: 5)
}
});
```
@@ -280,52 +280,52 @@ Requires password confirmation. Revokes trusted device records:
```ts
const disable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.disable({
password,
});
const { data, error } = await authClient.twoFactor.disable({
password
});
};
```
## Complete Configuration Example
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { sendEmail } from "./email";
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { sendEmail } from './email';
export const auth = betterAuth({
appName: "My App",
plugins: [
twoFactor({
// TOTP settings
issuer: "My App",
totpOptions: {
digits: 6,
period: 30,
},
// OTP settings
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({
to: user.email,
subject: "Your verification code",
text: `Your code is: ${otp}`,
});
},
period: 5,
allowedAttempts: 5,
storeOTP: "encrypted",
},
// Backup code settings
backupCodeOptions: {
amount: 10,
length: 10,
storeBackupCodes: "encrypted",
},
// Session settings
twoFactorCookieMaxAge: 600, // 10 minutes
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days
}),
],
appName: 'My App',
plugins: [
twoFactor({
// TOTP settings
issuer: 'My App',
totpOptions: {
digits: 6,
period: 30
},
// OTP settings
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({
to: user.email,
subject: 'Your verification code',
text: `Your code is: ${otp}`
});
},
period: 5,
allowedAttempts: 5,
storeOTP: 'encrypted'
},
// Backup code settings
backupCodeOptions: {
amount: 10,
length: 10,
storeBackupCodes: 'encrypted'
},
// Session settings
twoFactorCookieMaxAge: 600, // 10 minutes
trustDeviceMaxAge: 30 * 24 * 60 * 60 // 30 days
})
]
});
```