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:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user