better-auth-lead 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -34
- package/dist/client.d.mts +1 -1
- package/dist/index-BX1SlAVz.d.mts +472 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +246 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/index-P6m89iLp.d.mts +0 -251
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ const authClient = createAuthClient({
|
|
|
56
56
|
|
|
57
57
|
### Subscribe
|
|
58
58
|
|
|
59
|
+
Provide an `email` to subscribe an anonymous lead:
|
|
60
|
+
|
|
59
61
|
```ts
|
|
60
62
|
// POST /lead/subscribe
|
|
61
63
|
const { data, error } = await authClient.lead.subscribe({
|
|
@@ -67,6 +69,19 @@ const { data, error } = await authClient.lead.subscribe({
|
|
|
67
69
|
});
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
Or omit `email` to subscribe the currently authenticated user. The lead is associated to the session user's `id` (a valid session cookie is required):
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// POST /lead/subscribe
|
|
76
|
+
const { data, error } = await authClient.lead.subscribe({
|
|
77
|
+
metadata: {
|
|
78
|
+
preferences: 'engineering',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If neither `email` nor an active session is provided, the endpoint responds with `400 Bad Request` (`EMAIL_OR_SESSION_REQUIRED`).
|
|
84
|
+
|
|
70
85
|
### Verify
|
|
71
86
|
|
|
72
87
|
```ts
|
|
@@ -80,7 +95,7 @@ await authClient.lead.verify({
|
|
|
80
95
|
|
|
81
96
|
### Unsubscribe
|
|
82
97
|
|
|
83
|
-
The unsubscribe endpoint is designed for [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) one-click unsubscribe. The signed `token` is embedded in the `unsubscribeUrl` provided to `
|
|
98
|
+
The unsubscribe endpoint is designed for [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) one-click unsubscribe. The signed `token` is embedded in the `unsubscribeUrl` provided to `sendConfirmationEmail` and should be used in `List-Unsubscribe` email headers — email clients (Gmail, Apple Mail, Yahoo Mail) will POST to this URL automatically when the user clicks "Unsubscribe".
|
|
84
99
|
|
|
85
100
|
```ts
|
|
86
101
|
// POST /lead/unsubscribe?token=<signed-token>
|
|
@@ -89,8 +104,17 @@ const { data, error } = await authClient.lead.unsubscribe({
|
|
|
89
104
|
});
|
|
90
105
|
```
|
|
91
106
|
|
|
107
|
+
For an authenticated user (e.g. from a "Manage preferences" page in your app), use the session-based endpoint. It requires a valid session and deletes the lead associated with the session user's `id`:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// POST /lead/unsubscribe-session
|
|
111
|
+
const { data, error } = await authClient.lead.unsubscribeSession();
|
|
112
|
+
```
|
|
113
|
+
|
|
92
114
|
### Resend
|
|
93
115
|
|
|
116
|
+
Resend the confirmation email by `email`:
|
|
117
|
+
|
|
94
118
|
```ts
|
|
95
119
|
// POST /lead/resend
|
|
96
120
|
const { data, error } = await authClient.lead.resend({
|
|
@@ -98,25 +122,132 @@ const { data, error } = await authClient.lead.resend({
|
|
|
98
122
|
});
|
|
99
123
|
```
|
|
100
124
|
|
|
125
|
+
Or omit `email` to resend for the currently authenticated user (lead is looked up by the session user's `id`):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// POST /lead/resend
|
|
129
|
+
const { data, error } = await authClient.lead.resend();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If neither `email` nor an active session is provided, the endpoint responds with `400 Bad Request` (`EMAIL_OR_SESSION_REQUIRED`).
|
|
133
|
+
|
|
101
134
|
### Update
|
|
102
135
|
|
|
136
|
+
Update the metadata of the lead associated with the currently authenticated user. Requires a valid session — the lead is looked up by the session user's `id`:
|
|
137
|
+
|
|
103
138
|
```ts
|
|
104
139
|
// POST /lead/update
|
|
105
140
|
const { data, error } = await authClient.lead.update({
|
|
106
|
-
id: 'lead-id',
|
|
107
141
|
metadata: {
|
|
108
142
|
preferences: 'ai',
|
|
109
143
|
},
|
|
110
144
|
});
|
|
111
145
|
```
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
If no session is present the endpoint responds with `401 Unauthorized`.
|
|
148
|
+
|
|
149
|
+
### Admin
|
|
150
|
+
|
|
151
|
+
The lead plugin ships with a set of admin-only endpoints to manage leads: `listLead`, `getLead`, and `removeLead`. They are only available when:
|
|
152
|
+
|
|
153
|
+
1. The better-auth [`admin`](https://www.better-auth.com/docs/plugins/admin) plugin is registered.
|
|
154
|
+
2. The `admin.enabled` lead option is set to `true`.
|
|
155
|
+
|
|
156
|
+
Enable them in your auth config:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// server/auth.ts
|
|
160
|
+
import { betterAuth } from 'better-auth';
|
|
161
|
+
import { admin } from 'better-auth/plugins';
|
|
162
|
+
import { lead } from 'better-auth-lead';
|
|
163
|
+
|
|
164
|
+
export const auth = betterAuth({
|
|
165
|
+
plugins: [
|
|
166
|
+
admin(),
|
|
167
|
+
lead({
|
|
168
|
+
admin: {
|
|
169
|
+
enabled: true,
|
|
170
|
+
// Optional. Roles allowed to call the admin endpoints.
|
|
171
|
+
// Default: ['admin'].
|
|
172
|
+
// Checked against session.user.role (the admin plugin supports
|
|
173
|
+
// comma-separated roles).
|
|
174
|
+
roles: ['admin', 'editor'],
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### List leads
|
|
182
|
+
|
|
183
|
+
List a page of leads with optional search, filter, sort, and pagination. Requires a session with a role in `admin.roles`.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// GET /lead/list-leads
|
|
187
|
+
const { data, error } = await authClient.lead.listLeads({
|
|
188
|
+
query: {
|
|
189
|
+
searchValue: 'user@example.com', // optional
|
|
190
|
+
searchField: 'email', // optional, default 'email'
|
|
191
|
+
searchOperator: 'contains', // optional: 'contains' | 'starts_with' | 'ends_with'
|
|
192
|
+
filterField: 'confirmed', // optional
|
|
193
|
+
filterValue: true, // optional
|
|
194
|
+
filterOperator: 'eq', // optional: any better-auth where operator
|
|
195
|
+
sortBy: 'createdAt', // optional
|
|
196
|
+
sortDirection: 'desc', // optional: 'asc' | 'desc'
|
|
197
|
+
limit: 100, // optional, default 100
|
|
198
|
+
offset: 0, // optional, default 0
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The response includes the page of `leads`, the `total` number of leads in the database, and the resolved `limit` and `offset` for client-side pagination.
|
|
204
|
+
|
|
205
|
+
#### Get a lead
|
|
206
|
+
|
|
207
|
+
Fetch a single lead by `id`. Requires a session with a role in `admin.roles`.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// POST /lead/get-lead
|
|
211
|
+
const { data, error } = await authClient.lead.getLead({
|
|
212
|
+
query: {
|
|
213
|
+
id: 'lead-id',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Returns the lead object, or `404 Not Found` (`LEAD_NOT_FOUND`) if no lead matches the id.
|
|
219
|
+
|
|
220
|
+
#### Remove a lead
|
|
221
|
+
|
|
222
|
+
Delete a single lead by `id`. Requires a session with a role in `admin.roles`.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// POST /lead/remove-lead
|
|
226
|
+
const { data, error } = await authClient.lead.removeLead({
|
|
227
|
+
body: {
|
|
228
|
+
leadId: 'lead-id',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
```
|
|
114
232
|
|
|
115
|
-
|
|
233
|
+
Returns `{ success: true }` on success, or `404 Not Found` (`LEAD_NOT_FOUND`) if no lead matches the id.
|
|
234
|
+
|
|
235
|
+
#### Responses
|
|
236
|
+
|
|
237
|
+
All admin endpoints share the following responses:
|
|
238
|
+
|
|
239
|
+
- `404 Not Found` (`ADMIN_PLUGIN_REQUIRED`) if the admin plugin is not registered or `admin.enabled` is not set.
|
|
240
|
+
- `403 Forbidden` (`FORBIDDEN`) if the session user's role is not in `admin.roles`.
|
|
241
|
+
- `401 Unauthorized` if no session is present.
|
|
242
|
+
|
|
243
|
+
### Email Confirmation
|
|
244
|
+
|
|
245
|
+
To enable double opt-in email confirmation, pass a `sendConfirmationEmail` function. It receives a data object with:
|
|
116
246
|
|
|
117
247
|
- `lead`: The lead object.
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
248
|
+
- `email`: The lead's email address.
|
|
249
|
+
- `url`: The URL containing the confirmation token to send to the user.
|
|
250
|
+
- `token`: The confirmation token used to complete the verification.
|
|
120
251
|
- `unsubscribeUrl`: The endpoint URL for one-click unsubscribe (RFC 8058). Use this in `List-Unsubscribe` email headers.
|
|
121
252
|
|
|
122
253
|
and a `request` object as the second parameter.
|
|
@@ -130,22 +261,22 @@ import { sendEmail } from './email'; // your email sending function
|
|
|
130
261
|
export const auth = betterAuth({
|
|
131
262
|
plugins: [
|
|
132
263
|
lead({
|
|
133
|
-
|
|
134
|
-
const {
|
|
264
|
+
sendConfirmationEmail: async ({ lead, email, url, token, unsubscribeUrl }) => {
|
|
265
|
+
const { confirmationSentAt } = lead;
|
|
135
266
|
if (
|
|
136
|
-
|
|
137
|
-
Date.now() -
|
|
267
|
+
confirmationSentAt &&
|
|
268
|
+
Date.now() - confirmationSentAt.getTime() < 60 * 1000 // 1 minute
|
|
138
269
|
) {
|
|
139
270
|
console.log(
|
|
140
|
-
`Skipping sending
|
|
271
|
+
`Skipping sending confirmation email to ${email} because a recent email was already sent.`,
|
|
141
272
|
);
|
|
142
273
|
return false;
|
|
143
274
|
}
|
|
144
275
|
|
|
145
276
|
void sendEmail({
|
|
146
|
-
to:
|
|
147
|
-
subject: 'Newsletter:
|
|
148
|
-
text: `Click the link to
|
|
277
|
+
to: email,
|
|
278
|
+
subject: 'Newsletter: Confirm your subscription',
|
|
279
|
+
text: `Click the link to confirm your subscription: ${url}`,
|
|
149
280
|
// One-click unsubscribe headers (RFC 8058)
|
|
150
281
|
// Supported by Gmail, Apple Mail, and Yahoo Mail.
|
|
151
282
|
headers: {
|
|
@@ -156,9 +287,9 @@ export const auth = betterAuth({
|
|
|
156
287
|
|
|
157
288
|
return true;
|
|
158
289
|
},
|
|
159
|
-
|
|
160
|
-
// do something when a lead
|
|
161
|
-
console.log(`Lead ${lead
|
|
290
|
+
onConfirmed: async ({ lead }) => {
|
|
291
|
+
// do something when a lead confirms their subscription
|
|
292
|
+
console.log(`Lead ${lead} has confirmed their subscription!`);
|
|
162
293
|
},
|
|
163
294
|
}),
|
|
164
295
|
],
|
|
@@ -167,7 +298,7 @@ export const auth = betterAuth({
|
|
|
167
298
|
|
|
168
299
|
> Avoid awaiting the email sending to prevent timing attacks.
|
|
169
300
|
|
|
170
|
-
Additionally, you can provide an `
|
|
301
|
+
Additionally, you can provide an `onConfirmed` callback to execute logic after a lead confirms their subscription.
|
|
171
302
|
|
|
172
303
|
### Metadata Validation
|
|
173
304
|
|
|
@@ -218,6 +349,11 @@ await authClient.lead.subscribe({
|
|
|
218
349
|
email: 'user@example.com',
|
|
219
350
|
metadata: { preferences: 'engineering' },
|
|
220
351
|
});
|
|
352
|
+
|
|
353
|
+
// or for the currently authenticated user (omit email)
|
|
354
|
+
await authClient.lead.subscribe({
|
|
355
|
+
metadata: { preferences: 'engineering' },
|
|
356
|
+
});
|
|
221
357
|
```
|
|
222
358
|
|
|
223
359
|
## Schema
|
|
@@ -226,29 +362,33 @@ await authClient.lead.subscribe({
|
|
|
226
362
|
|
|
227
363
|
Table name: `lead`
|
|
228
364
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
| id
|
|
232
|
-
| email
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
365
|
+
| Field | Type | Key | Description |
|
|
366
|
+
| ------------------ | ------- | ------ | ------------------------------------------------- |
|
|
367
|
+
| id | string | pk | Unique identifier for each lead |
|
|
368
|
+
| email | string? | unique | Email address of the lead (optional) |
|
|
369
|
+
| userId | string? | unique | ID of an associated better-auth user (optional) |
|
|
370
|
+
| confirmed | boolean | | Whether the lead has confirmed their subscription |
|
|
371
|
+
| confirmationSentAt | Date | ? | Timestamp of when the confirmation email was sent |
|
|
372
|
+
| metadata | json | ? | Additional data about the lead |
|
|
373
|
+
| createdAt | date | | Timestamp of lead creation |
|
|
374
|
+
| updatedAt | date | | Timestamp of last update |
|
|
238
375
|
|
|
239
376
|
#### Prisma
|
|
240
377
|
|
|
241
378
|
```prisma
|
|
242
379
|
model Lead {
|
|
243
|
-
id
|
|
244
|
-
createdAt
|
|
245
|
-
updatedAt
|
|
246
|
-
email
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
380
|
+
id String @id
|
|
381
|
+
createdAt DateTime @default(now())
|
|
382
|
+
updatedAt DateTime @updatedAt
|
|
383
|
+
email String?
|
|
384
|
+
userId String?
|
|
385
|
+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
386
|
+
confirmed Boolean @default(false)
|
|
387
|
+
confirmationSentAt DateTime?
|
|
388
|
+
metadata String?
|
|
250
389
|
|
|
251
390
|
@@unique([email])
|
|
391
|
+
@@unique([userId])
|
|
252
392
|
@@map("lead")
|
|
253
393
|
}
|
|
254
394
|
```
|
package/dist/client.d.mts
CHANGED