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
@@ -8,14 +8,15 @@ description: Configure rate limiting, manage auth secrets, set up CSRF protectio
### Configuring the Secret
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env
secret: process.env.BETTER_AUTH_SECRET // or via `BETTER_AUTH_SECRET` env
});
```
Better Auth looks for secrets in this order:
1. `options.secret` in your config
2. `BETTER_AUTH_SECRET` environment variable
3. `AUTH_SECRET` environment variable
@@ -34,14 +35,14 @@ Enabled in production by default. Applies to all endpoints. Plugins can override
### Default Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
rateLimit: {
enabled: true, // Default: true in production
window: 10, // Time window in seconds (default: 10)
max: 100, // Max requests per window (default: 100)
},
rateLimit: {
enabled: true, // Default: true in production
window: 10, // Time window in seconds (default: 10)
max: 100 // Max requests per window (default: 100)
}
});
```
@@ -95,12 +96,12 @@ Multi-layer protection: origin header validation, Fetch Metadata checks, and fir
### Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
disableCSRFCheck: false, // Default: false (keep enabled)
},
advanced: {
disableCSRFCheck: false // Default: false (keep enabled)
}
});
```
@@ -111,14 +112,11 @@ Only disable for testing or with an alternative CSRF mechanism.
### Configuring Trusted Origins
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://admin.example.com",
],
baseURL: 'https://api.example.com',
trustedOrigins: ['https://app.example.com', 'https://admin.example.com']
});
```
@@ -128,10 +126,10 @@ The `baseURL` origin is automatically trusted. Also configurable via env: `BETTE
```ts
trustedOrigins: [
"*.example.com", // Matches any subdomain
"https://*.example.com", // Protocol-specific wildcard
"exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo)
]
'*.example.com', // Matches any subdomain
'https://*.example.com', // Protocol-specific wildcard
'exp://192.168.*.*:*/*' // Custom schemes (e.g., Expo)
];
```
### Dynamic Trusted Origins
@@ -140,10 +138,10 @@ Compute trusted origins based on the request:
```ts
trustedOrigins: async (request) => {
// Validate against database, header, etc.
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.myapp.com`];
}
// Validate against database, header, etc.
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.myapp.com`];
};
```
Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403.
@@ -153,13 +151,13 @@ Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`,
### Session Expiration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default)
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
updateAge: 60 * 60 * 24 // Refresh session every 24 hours (default)
}
});
```
@@ -186,17 +184,17 @@ Defaults: `secure: true` (HTTPS/production), `sameSite: "lax"`, `httpOnly: true`
### Custom Cookie Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
useSecureCookies: true, // Force secure cookies
cookiePrefix: "myapp", // Custom prefix (default: "better-auth")
defaultCookieAttributes: {
sameSite: "strict", // Stricter CSRF protection
path: "/auth", // Limit cookie scope
},
},
advanced: {
useSecureCookies: true, // Force secure cookies
cookiePrefix: 'myapp', // Custom prefix (default: "better-auth")
defaultCookieAttributes: {
sameSite: 'strict', // Stricter CSRF protection
path: '/auth' // Limit cookie scope
}
}
});
```
@@ -221,12 +219,12 @@ PKCE is automatic for all OAuth flows. State tokens are 32-char random strings e
### State Parameter Storage
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
account: {
storeStateStrategy: "cookie", // Options: "cookie" (default), "database"
},
account: {
storeStateStrategy: 'cookie' // Options: "cookie" (default), "database"
}
});
```
@@ -245,15 +243,15 @@ Enable if storing OAuth tokens for API access on behalf of users. Use `skipState
### IP Address Configuration
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check
disableIpTracking: false, // Keep enabled for rate limiting
},
},
advanced: {
ipAddress: {
ipAddressHeaders: ['x-forwarded-for', 'x-real-ip'], // Headers to check
disableIpTracking: false // Keep enabled for rate limiting
}
}
});
```
@@ -262,50 +260,50 @@ Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `
## Database Hooks for Security Auditing
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog("session.created", {
userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"),
userAgent: ctx?.request?.headers.get("user-agent"),
});
},
},
delete: {
before: async ({ data }) => {
await auditLog("session.revoked", { sessionId: data.id });
},
},
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
await auditLog("user.email_changed", {
userId: data.id,
oldEmail: oldData?.email,
newEmail: data.email,
});
}
},
},
},
account: {
create: {
after: async ({ data }) => {
await auditLog("account.linked", {
userId: data.userId,
provider: data.providerId,
});
},
},
},
},
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog('session.created', {
userId: data.userId,
ip: ctx?.request?.headers.get('x-forwarded-for'),
userAgent: ctx?.request?.headers.get('user-agent')
});
}
},
delete: {
before: async ({ data }) => {
await auditLog('session.revoked', { sessionId: data.id });
}
}
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
await auditLog('user.email_changed', {
userId: data.id,
oldEmail: oldData?.email,
newEmail: data.email
});
}
}
}
},
account: {
create: {
after: async ({ data }) => {
await auditLog('account.linked', {
userId: data.userId,
provider: data.providerId
});
}
}
}
}
});
```
@@ -314,19 +312,19 @@ Return `false` from a `before` hook to prevent an operation.
## Background Tasks
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
advanced: {
backgroundTasks: {
handler: (promise) => {
// Platform-specific handler
// Vercel: waitUntil(promise)
// Cloudflare: ctx.waitUntil(promise)
waitUntil(promise);
},
},
},
advanced: {
backgroundTasks: {
handler: (promise) => {
// Platform-specific handler
// Vercel: waitUntil(promise)
// Cloudflare: ctx.waitUntil(promise)
waitUntil(promise);
}
}
}
});
```
@@ -339,80 +337,76 @@ Built-in: consistent response messages, dummy operations on invalid requests, ba
## Complete Security Configuration Example
```ts
import { betterAuth } from "better-auth";
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://*.preview.example.com",
],
// Rate limiting
rateLimit: {
enabled: true,
storage: "secondary-storage",
customRules: {
"/api/auth/sign-in/email": { window: 60, max: 5 },
"/api/auth/sign-up/email": { window: 60, max: 3 },
},
},
// Session security
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 24 hours
freshAge: 60 * 60, // 1 hour for sensitive actions
cookieCache: {
enabled: true,
maxAge: 300,
strategy: "jwe", // Encrypted session data
},
},
// OAuth security
account: {
encryptOAuthTokens: true,
storeStateStrategy: "cookie",
},
// Advanced settings
advanced: {
useSecureCookies: true,
cookiePrefix: "myapp",
defaultCookieAttributes: {
sameSite: "lax",
},
ipAddress: {
ipAddressHeaders: ["x-forwarded-for"],
ipv6Subnet: 64,
},
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
},
// Security auditing
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
console.log(`New session for user ${data.userId}`);
},
},
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
console.log(`Email changed for user ${data.id}`);
}
},
},
},
},
secret: process.env.BETTER_AUTH_SECRET,
baseURL: 'https://api.example.com',
trustedOrigins: ['https://app.example.com', 'https://*.preview.example.com'],
// Rate limiting
rateLimit: {
enabled: true,
storage: 'secondary-storage',
customRules: {
'/api/auth/sign-in/email': { window: 60, max: 5 },
'/api/auth/sign-up/email': { window: 60, max: 3 }
}
},
// Session security
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 24 hours
freshAge: 60 * 60, // 1 hour for sensitive actions
cookieCache: {
enabled: true,
maxAge: 300,
strategy: 'jwe' // Encrypted session data
}
},
// OAuth security
account: {
encryptOAuthTokens: true,
storeStateStrategy: 'cookie'
},
// Advanced settings
advanced: {
useSecureCookies: true,
cookiePrefix: 'myapp',
defaultCookieAttributes: {
sameSite: 'lax'
},
ipAddress: {
ipAddressHeaders: ['x-forwarded-for'],
ipv6Subnet: 64
},
backgroundTasks: {
handler: (promise) => waitUntil(promise)
}
},
// Security auditing
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
console.log(`New session for user ${data.userId}`);
}
}
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
console.log(`Email changed for user ${data.id}`);
}
}
}
}
}
});
```