@contentgrowth/content-auth 0.2.8 → 0.3.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/dist/backend/index.d.ts +62 -1
- package/dist/backend/index.js +9 -3
- package/dist/{chunk-N5OK3XPK.js → chunk-H37QRFRH.js} +192 -2
- package/dist/{chunk-4R4FSU2G.js → chunk-XL5CRGGU.js} +88 -11
- package/dist/frontend/client.d.ts +8 -8
- package/dist/frontend/index.d.ts +2 -0
- package/dist/frontend/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -4
- package/dist/styles.css +23 -0
- package/package.json +9 -3
- package/schema/auth.sql +2 -0
package/dist/backend/index.d.ts
CHANGED
|
@@ -6,6 +6,47 @@ import { Context, Hono } from 'hono';
|
|
|
6
6
|
export { Hono } from 'hono';
|
|
7
7
|
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Cloudflare Turnstile Verification Utility
|
|
11
|
+
*
|
|
12
|
+
* Verifies Turnstile tokens on the server-side using Cloudflare's Siteverify API.
|
|
13
|
+
* @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
|
|
14
|
+
*/
|
|
15
|
+
interface VerifyResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
hostname?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verifies a Turnstile token with Cloudflare's Siteverify API.
|
|
22
|
+
*
|
|
23
|
+
* @param secretKey - Your Turnstile secret key
|
|
24
|
+
* @param token - The token from the client-side Turnstile widget (cf-turnstile-response)
|
|
25
|
+
* @param remoteIp - Optional: The visitor's IP address for additional validation
|
|
26
|
+
* @returns Verification result with success status and any error messages
|
|
27
|
+
*/
|
|
28
|
+
declare function verifyTurnstile(secretKey: string, token: string, remoteIp?: string): Promise<VerifyResult>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Email Normalization Utilities
|
|
32
|
+
*
|
|
33
|
+
* Normalizes email addresses to prevent duplicate accounts through
|
|
34
|
+
* the "Gmail dot trick" and plus-addressing.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes an email address for duplicate detection.
|
|
38
|
+
* - Gmail addresses: strips dots and plus-addressing, normalizes googlemail.com
|
|
39
|
+
* - Other addresses: lowercases only
|
|
40
|
+
*
|
|
41
|
+
* @param email - The email address to normalize
|
|
42
|
+
* @returns The normalized email address
|
|
43
|
+
*/
|
|
44
|
+
declare function normalizeEmail(email: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Checks if an email is a Gmail address (including googlemail.com)
|
|
47
|
+
*/
|
|
48
|
+
declare function isGmailAddress(email: string): boolean;
|
|
49
|
+
|
|
9
50
|
declare const users: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
10
51
|
name: "users";
|
|
11
52
|
schema: undefined;
|
|
@@ -1084,6 +1125,16 @@ declare function getInvitationLink(data: any, baseUrl: string): {
|
|
|
1084
1125
|
*/
|
|
1085
1126
|
declare function getSessionToken(req: Request): string | null;
|
|
1086
1127
|
|
|
1128
|
+
interface TurnstileConfig {
|
|
1129
|
+
/** Cloudflare Turnstile secret key */
|
|
1130
|
+
secretKey: string;
|
|
1131
|
+
}
|
|
1132
|
+
interface EmailNormalizationConfig {
|
|
1133
|
+
/** Enable email normalization for duplicate detection. Default: false */
|
|
1134
|
+
enabled: boolean;
|
|
1135
|
+
/** Column name in users table. Default: 'normalized_email' */
|
|
1136
|
+
columnName?: string;
|
|
1137
|
+
}
|
|
1087
1138
|
interface AuthConfig {
|
|
1088
1139
|
/**
|
|
1089
1140
|
* The database instance or D1 binding.
|
|
@@ -1121,6 +1172,16 @@ interface AuthConfig {
|
|
|
1121
1172
|
token: string;
|
|
1122
1173
|
}, request: any) => Promise<void> | void;
|
|
1123
1174
|
};
|
|
1175
|
+
/**
|
|
1176
|
+
* Cloudflare Turnstile configuration for bot protection on email signups.
|
|
1177
|
+
* When configured, the AuthForm frontend will show a Turnstile challenge.
|
|
1178
|
+
*/
|
|
1179
|
+
turnstile?: TurnstileConfig;
|
|
1180
|
+
/**
|
|
1181
|
+
* Email normalization for duplicate prevention (Gmail dot trick, plus-addressing).
|
|
1182
|
+
* Requires a 'normalized_email' column in the users table.
|
|
1183
|
+
*/
|
|
1184
|
+
emailNormalization?: EmailNormalizationConfig;
|
|
1124
1185
|
[key: string]: any;
|
|
1125
1186
|
}
|
|
1126
1187
|
declare const createAuth: (config: AuthConfig) => better_auth.Auth<any>;
|
|
@@ -1130,4 +1191,4 @@ declare const createAuthApp: (config: AuthConfig) => {
|
|
|
1130
1191
|
auth: better_auth.Auth<any>;
|
|
1131
1192
|
};
|
|
1132
1193
|
|
|
1133
|
-
export { type AuthConfig, authMiddleware, createAuth, createAuthApp, getInvitationLink, getSessionToken, schema };
|
|
1194
|
+
export { type AuthConfig, type EmailNormalizationConfig, type TurnstileConfig, authMiddleware, createAuth, createAuthApp, getInvitationLink, getSessionToken, isGmailAddress, normalizeEmail, schema, verifyTurnstile };
|
package/dist/backend/index.js
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
createAuthApp,
|
|
6
6
|
getInvitationLink,
|
|
7
7
|
getSessionToken,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
isGmailAddress,
|
|
9
|
+
normalizeEmail,
|
|
10
|
+
schema_exports,
|
|
11
|
+
verifyTurnstile
|
|
12
|
+
} from "../chunk-H37QRFRH.js";
|
|
10
13
|
import "../chunk-R5U7XKVJ.js";
|
|
11
14
|
export {
|
|
12
15
|
Hono,
|
|
@@ -15,5 +18,8 @@ export {
|
|
|
15
18
|
createAuthApp,
|
|
16
19
|
getInvitationLink,
|
|
17
20
|
getSessionToken,
|
|
18
|
-
|
|
21
|
+
isGmailAddress,
|
|
22
|
+
normalizeEmail,
|
|
23
|
+
schema_exports as schema,
|
|
24
|
+
verifyTurnstile
|
|
19
25
|
};
|
|
@@ -9,6 +9,87 @@ import { drizzle } from "drizzle-orm/d1";
|
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
export * from "better-auth";
|
|
11
11
|
|
|
12
|
+
// src/backend/turnstile.ts
|
|
13
|
+
async function verifyTurnstile(secretKey, token, remoteIp) {
|
|
14
|
+
if (!secretKey) {
|
|
15
|
+
console.warn("[Turnstile] No secret key configured, skipping verification");
|
|
16
|
+
return { success: true };
|
|
17
|
+
}
|
|
18
|
+
if (!token) {
|
|
19
|
+
return { success: false, error: "Missing Turnstile token" };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const formData = new URLSearchParams();
|
|
23
|
+
formData.append("secret", secretKey);
|
|
24
|
+
formData.append("response", token);
|
|
25
|
+
if (remoteIp) {
|
|
26
|
+
formData.append("remoteip", remoteIp);
|
|
27
|
+
}
|
|
28
|
+
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
32
|
+
},
|
|
33
|
+
body: formData.toString()
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
console.error(`[Turnstile] Siteverify API error: ${response.status}`);
|
|
37
|
+
return { success: false, error: `API error: ${response.status}` };
|
|
38
|
+
}
|
|
39
|
+
const result = await response.json();
|
|
40
|
+
if (result.success) {
|
|
41
|
+
return { success: true, hostname: result.hostname };
|
|
42
|
+
} else {
|
|
43
|
+
const errorCodes = result["error-codes"] || [];
|
|
44
|
+
console.warn(`[Turnstile] Verification failed: ${errorCodes.join(", ")}`);
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: mapTurnstileError(errorCodes)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[Turnstile] Verification error: ${error.message}`);
|
|
52
|
+
return { success: false, error: "Verification service unavailable" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mapTurnstileError(codes) {
|
|
56
|
+
if (codes.includes("timeout-or-duplicate")) {
|
|
57
|
+
return "Challenge expired or already used. Please try again.";
|
|
58
|
+
}
|
|
59
|
+
if (codes.includes("invalid-input-response")) {
|
|
60
|
+
return "Invalid challenge response. Please try again.";
|
|
61
|
+
}
|
|
62
|
+
if (codes.includes("bad-request")) {
|
|
63
|
+
return "Invalid request. Please refresh and try again.";
|
|
64
|
+
}
|
|
65
|
+
if (codes.includes("internal-error")) {
|
|
66
|
+
return "Verification service error. Please try again later.";
|
|
67
|
+
}
|
|
68
|
+
return "Challenge verification failed. Please try again.";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/backend/email-normalize.ts
|
|
72
|
+
function normalizeEmail(email) {
|
|
73
|
+
const lowerEmail = email.toLowerCase().trim();
|
|
74
|
+
const atIndex = lowerEmail.indexOf("@");
|
|
75
|
+
if (atIndex === -1) {
|
|
76
|
+
return lowerEmail;
|
|
77
|
+
}
|
|
78
|
+
const local = lowerEmail.substring(0, atIndex);
|
|
79
|
+
const domain = lowerEmail.substring(atIndex + 1);
|
|
80
|
+
const normalizedDomain = domain === "googlemail.com" ? "gmail.com" : domain;
|
|
81
|
+
if (normalizedDomain === "gmail.com") {
|
|
82
|
+
const localWithoutPlus = local.split("+")[0];
|
|
83
|
+
const normalizedLocal = localWithoutPlus.replace(/\./g, "");
|
|
84
|
+
return `${normalizedLocal}@gmail.com`;
|
|
85
|
+
}
|
|
86
|
+
return lowerEmail;
|
|
87
|
+
}
|
|
88
|
+
function isGmailAddress(email) {
|
|
89
|
+
const lowerEmail = email.toLowerCase();
|
|
90
|
+
return lowerEmail.endsWith("@gmail.com") || lowerEmail.endsWith("@googlemail.com");
|
|
91
|
+
}
|
|
92
|
+
|
|
12
93
|
// src/backend/schema.ts
|
|
13
94
|
var schema_exports = {};
|
|
14
95
|
__export(schema_exports, {
|
|
@@ -195,13 +276,24 @@ function getSessionToken(req) {
|
|
|
195
276
|
// src/backend/index.ts
|
|
196
277
|
var createAuth = (config) => {
|
|
197
278
|
let db;
|
|
279
|
+
let rawDb = config.database;
|
|
198
280
|
let provider = config.provider || "sqlite";
|
|
199
281
|
if (config.database && typeof config.database.prepare === "function") {
|
|
200
282
|
db = drizzle(config.database, { schema: schema_exports });
|
|
201
283
|
} else {
|
|
202
284
|
db = config.database;
|
|
203
285
|
}
|
|
204
|
-
const {
|
|
286
|
+
const {
|
|
287
|
+
database,
|
|
288
|
+
secret,
|
|
289
|
+
baseUrl,
|
|
290
|
+
provider: _,
|
|
291
|
+
useCloudflareNativeHashing = true,
|
|
292
|
+
emailVerification,
|
|
293
|
+
turnstile: turnstileConfig,
|
|
294
|
+
emailNormalization,
|
|
295
|
+
...rest
|
|
296
|
+
} = config;
|
|
205
297
|
let adapterOptions = {
|
|
206
298
|
provider,
|
|
207
299
|
schema: {
|
|
@@ -215,7 +307,7 @@ var createAuth = (config) => {
|
|
|
215
307
|
}
|
|
216
308
|
};
|
|
217
309
|
const emailConfig = rest.emailAndPassword || { enabled: true };
|
|
218
|
-
const { emailAndPassword, ...otherOptions } = rest;
|
|
310
|
+
const { emailAndPassword, hooks: userHooks, ...otherOptions } = rest;
|
|
219
311
|
const emailPasswordOptions = {
|
|
220
312
|
...emailConfig
|
|
221
313
|
};
|
|
@@ -225,6 +317,99 @@ var createAuth = (config) => {
|
|
|
225
317
|
verify: verifyPassword
|
|
226
318
|
};
|
|
227
319
|
}
|
|
320
|
+
const normalizedEmailColumn = emailNormalization?.columnName || "normalized_email";
|
|
321
|
+
const contentAuthHooks = {
|
|
322
|
+
before: async (context) => {
|
|
323
|
+
const path = context.path || "";
|
|
324
|
+
if (path.includes("/sign-up/email")) {
|
|
325
|
+
try {
|
|
326
|
+
let body;
|
|
327
|
+
if (context.body) {
|
|
328
|
+
body = context.body;
|
|
329
|
+
} else {
|
|
330
|
+
try {
|
|
331
|
+
body = await context.request.clone().json();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (body) {
|
|
336
|
+
if (turnstileConfig?.secretKey) {
|
|
337
|
+
const turnstileToken = body.turnstileToken;
|
|
338
|
+
if (!turnstileToken) {
|
|
339
|
+
console.warn("[ContentAuth] Email signup missing Turnstile token");
|
|
340
|
+
return {
|
|
341
|
+
status: 400,
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
success: false,
|
|
344
|
+
code: "TURNSTILE_REQUIRED",
|
|
345
|
+
message: "Please complete the security challenge"
|
|
346
|
+
}),
|
|
347
|
+
headers: { "Content-Type": "application/json" }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const { success, error } = await verifyTurnstile(turnstileConfig.secretKey, turnstileToken);
|
|
351
|
+
if (!success) {
|
|
352
|
+
console.warn(`[ContentAuth] Turnstile verification failed: ${error}`);
|
|
353
|
+
return {
|
|
354
|
+
status: 400,
|
|
355
|
+
body: JSON.stringify({
|
|
356
|
+
success: false,
|
|
357
|
+
code: "TURNSTILE_FAILED",
|
|
358
|
+
message: error || "Security challenge failed. Please try again."
|
|
359
|
+
}),
|
|
360
|
+
headers: { "Content-Type": "application/json" }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (emailNormalization?.enabled && body.email && rawDb?.prepare) {
|
|
365
|
+
const normalized = normalizeEmail(body.email);
|
|
366
|
+
const existing = await rawDb.prepare(
|
|
367
|
+
`SELECT id FROM users WHERE ${normalizedEmailColumn} = ?`
|
|
368
|
+
).bind(normalized).first();
|
|
369
|
+
if (existing) {
|
|
370
|
+
console.warn(`[ContentAuth] Duplicate normalized email detected: ${normalized}`);
|
|
371
|
+
return {
|
|
372
|
+
status: 400,
|
|
373
|
+
body: JSON.stringify({
|
|
374
|
+
success: false,
|
|
375
|
+
code: "EMAIL_EXISTS",
|
|
376
|
+
message: "An account with this email already exists"
|
|
377
|
+
}),
|
|
378
|
+
headers: { "Content-Type": "application/json" }
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.error("[ContentAuth] Email signup hook error:", e.message);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (userHooks?.before) {
|
|
388
|
+
return userHooks.before(context);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
},
|
|
392
|
+
after: async (context) => {
|
|
393
|
+
const path = context.path || "";
|
|
394
|
+
const user = context.user || context.response?.user || context.data?.user || context.context?.returned?.user || context.context?.newSession?.user;
|
|
395
|
+
if (emailNormalization?.enabled && rawDb?.prepare) {
|
|
396
|
+
if ((path.includes("/sign-up") || path.includes("/callback")) && user?.id && user?.email) {
|
|
397
|
+
try {
|
|
398
|
+
const normalized = normalizeEmail(user.email);
|
|
399
|
+
await rawDb.prepare(
|
|
400
|
+
`UPDATE users SET ${normalizedEmailColumn} = ? WHERE id = ? AND (${normalizedEmailColumn} IS NULL OR ${normalizedEmailColumn} != ?)`
|
|
401
|
+
).bind(normalized, user.id, normalized).run();
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.error(`[ContentAuth] Failed to set normalized_email: ${e.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (userHooks?.after) {
|
|
408
|
+
return userHooks.after(context);
|
|
409
|
+
}
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
};
|
|
228
413
|
const auth = betterAuth({
|
|
229
414
|
database: drizzleAdapter(db, adapterOptions),
|
|
230
415
|
secret,
|
|
@@ -232,6 +417,8 @@ var createAuth = (config) => {
|
|
|
232
417
|
emailAndPassword: emailPasswordOptions,
|
|
233
418
|
// Pass emailVerification config if provided
|
|
234
419
|
...emailVerification ? { emailVerification } : {},
|
|
420
|
+
// Merge content-auth hooks with user hooks
|
|
421
|
+
hooks: contentAuthHooks,
|
|
235
422
|
...otherOptions
|
|
236
423
|
});
|
|
237
424
|
return auth;
|
|
@@ -251,6 +438,9 @@ var createAuthApp = (config) => {
|
|
|
251
438
|
};
|
|
252
439
|
|
|
253
440
|
export {
|
|
441
|
+
verifyTurnstile,
|
|
442
|
+
normalizeEmail,
|
|
443
|
+
isGmailAddress,
|
|
254
444
|
schema_exports,
|
|
255
445
|
getInvitationLink,
|
|
256
446
|
getSessionToken,
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
authClient
|
|
3
3
|
} from "./chunk-NKDKDBM2.js";
|
|
4
|
+
import {
|
|
5
|
+
__require
|
|
6
|
+
} from "./chunk-R5U7XKVJ.js";
|
|
4
7
|
|
|
5
8
|
// src/frontend/components/AuthForm.tsx
|
|
6
|
-
import
|
|
9
|
+
import { useState, useRef, useEffect } from "react";
|
|
7
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
var TurnstileComponent = null;
|
|
12
|
+
var useTurnstile = false;
|
|
13
|
+
try {
|
|
14
|
+
const turnstileModule = __require("@marsidev/react-turnstile");
|
|
15
|
+
TurnstileComponent = turnstileModule.Turnstile;
|
|
16
|
+
useTurnstile = true;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
}
|
|
8
19
|
var AuthForm = ({
|
|
9
20
|
client = authClient,
|
|
10
21
|
onSuccess,
|
|
@@ -19,7 +30,8 @@ var AuthForm = ({
|
|
|
19
30
|
onSwitchMode,
|
|
20
31
|
defaultEmail = "",
|
|
21
32
|
lockEmail = false,
|
|
22
|
-
forgotPasswordUrl
|
|
33
|
+
forgotPasswordUrl,
|
|
34
|
+
turnstileSiteKey
|
|
23
35
|
}) => {
|
|
24
36
|
const [isLogin, setIsLogin] = useState(view !== "signup");
|
|
25
37
|
const [email, setEmail] = useState(defaultEmail);
|
|
@@ -27,13 +39,33 @@ var AuthForm = ({
|
|
|
27
39
|
const [name, setName] = useState("");
|
|
28
40
|
const [loading, setLoading] = useState(false);
|
|
29
41
|
const [error, setError] = useState(null);
|
|
42
|
+
const [turnstileToken, setTurnstileToken] = useState(null);
|
|
43
|
+
const turnstileRef = useRef(null);
|
|
30
44
|
const [mounted, setMounted] = useState(false);
|
|
31
|
-
|
|
45
|
+
useEffect(() => {
|
|
32
46
|
setMounted(true);
|
|
33
47
|
setIsLogin(view !== "signup");
|
|
34
48
|
}, [view]);
|
|
49
|
+
const turnstileEnabled = turnstileSiteKey && useTurnstile && TurnstileComponent;
|
|
50
|
+
const turnstileRequired = turnstileEnabled && !isLogin;
|
|
51
|
+
const canSubmit = !turnstileRequired || !!turnstileToken;
|
|
52
|
+
const handleTurnstileSuccess = (token) => {
|
|
53
|
+
setTurnstileToken(token);
|
|
54
|
+
};
|
|
55
|
+
const handleTurnstileError = () => {
|
|
56
|
+
setTurnstileToken(null);
|
|
57
|
+
setError("Security verification failed. Please try again.");
|
|
58
|
+
};
|
|
59
|
+
const handleTurnstileExpire = () => {
|
|
60
|
+
setTurnstileToken(null);
|
|
61
|
+
turnstileRef.current?.reset();
|
|
62
|
+
};
|
|
35
63
|
const handleSubmit = async (e) => {
|
|
36
64
|
e.preventDefault();
|
|
65
|
+
if (turnstileRequired && !turnstileToken) {
|
|
66
|
+
setError("Please complete the security challenge");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
37
69
|
setLoading(true);
|
|
38
70
|
setError(null);
|
|
39
71
|
try {
|
|
@@ -45,16 +77,24 @@ var AuthForm = ({
|
|
|
45
77
|
});
|
|
46
78
|
if (response.error) throw response.error;
|
|
47
79
|
} else {
|
|
48
|
-
|
|
80
|
+
const signupData = {
|
|
49
81
|
email,
|
|
50
82
|
password,
|
|
51
83
|
name
|
|
52
|
-
}
|
|
84
|
+
};
|
|
85
|
+
if (turnstileToken) {
|
|
86
|
+
signupData.turnstileToken = turnstileToken;
|
|
87
|
+
}
|
|
88
|
+
response = await client.signUp.email(signupData);
|
|
53
89
|
if (response.error) throw response.error;
|
|
54
90
|
}
|
|
55
91
|
onSuccess?.(response.data);
|
|
56
92
|
} catch (err) {
|
|
57
93
|
setError(err.message || "An error occurred");
|
|
94
|
+
if (turnstileRef.current) {
|
|
95
|
+
turnstileRef.current.reset();
|
|
96
|
+
setTurnstileToken(null);
|
|
97
|
+
}
|
|
58
98
|
} finally {
|
|
59
99
|
setLoading(false);
|
|
60
100
|
}
|
|
@@ -77,6 +117,26 @@ var AuthForm = ({
|
|
|
77
117
|
else if (width === "wide") widthClass = "ca-width-wide";
|
|
78
118
|
else widthClass = "ca-width-default";
|
|
79
119
|
const containerClass = `ca-container ${layout === "split" ? "ca-layout-split" : ""} ${widthClass} ${className || ""}`;
|
|
120
|
+
const renderTurnstile = () => {
|
|
121
|
+
if (!turnstileEnabled || isLogin) return null;
|
|
122
|
+
return /* @__PURE__ */ jsxs("div", { className: "ca-turnstile", children: [
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
TurnstileComponent,
|
|
125
|
+
{
|
|
126
|
+
ref: turnstileRef,
|
|
127
|
+
siteKey: turnstileSiteKey,
|
|
128
|
+
onSuccess: handleTurnstileSuccess,
|
|
129
|
+
onError: handleTurnstileError,
|
|
130
|
+
onExpire: handleTurnstileExpire,
|
|
131
|
+
options: {
|
|
132
|
+
theme: "light",
|
|
133
|
+
size: "normal"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
!turnstileToken && /* @__PURE__ */ jsx("p", { className: "ca-turnstile-hint", children: "Please complete the security check above" })
|
|
138
|
+
] });
|
|
139
|
+
};
|
|
80
140
|
const renderSocials = () => (
|
|
81
141
|
// Hide social logins when email is locked (e.g., invitation flow)
|
|
82
142
|
!lockEmail && socialProviders.length > 0 && /* @__PURE__ */ jsx("div", { className: socialClass, children: socialProviders.map((provider) => /* @__PURE__ */ jsxs(
|
|
@@ -154,8 +214,17 @@ var AuthForm = ({
|
|
|
154
214
|
}
|
|
155
215
|
)
|
|
156
216
|
] }),
|
|
217
|
+
renderTurnstile(),
|
|
157
218
|
error && /* @__PURE__ */ jsx("div", { className: "ca-error", children: error }),
|
|
158
|
-
/* @__PURE__ */ jsx(
|
|
219
|
+
/* @__PURE__ */ jsx(
|
|
220
|
+
"button",
|
|
221
|
+
{
|
|
222
|
+
type: "submit",
|
|
223
|
+
className: "ca-button",
|
|
224
|
+
disabled: loading || !canSubmit,
|
|
225
|
+
children: loading ? "Loading..." : isLogin ? "Sign In" : "Sign Up"
|
|
226
|
+
}
|
|
227
|
+
)
|
|
159
228
|
] });
|
|
160
229
|
};
|
|
161
230
|
const renderFooter = () => /* @__PURE__ */ jsxs("div", { className: "ca-footer", children: [
|
|
@@ -170,6 +239,10 @@ var AuthForm = ({
|
|
|
170
239
|
} else {
|
|
171
240
|
setIsLogin(!isLogin);
|
|
172
241
|
}
|
|
242
|
+
if (turnstileRef.current) {
|
|
243
|
+
turnstileRef.current.reset();
|
|
244
|
+
setTurnstileToken(null);
|
|
245
|
+
}
|
|
173
246
|
},
|
|
174
247
|
type: "button",
|
|
175
248
|
children: isLogin ? "Sign up" : "Sign in"
|
|
@@ -411,7 +484,7 @@ var ResetPasswordForm = ({
|
|
|
411
484
|
};
|
|
412
485
|
|
|
413
486
|
// src/frontend/components/Organization.tsx
|
|
414
|
-
import { useState as useState4, useEffect } from "react";
|
|
487
|
+
import { useState as useState4, useEffect as useEffect2 } from "react";
|
|
415
488
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
416
489
|
var CreateOrganizationForm = ({
|
|
417
490
|
client = authClient,
|
|
@@ -484,7 +557,7 @@ var OrganizationSwitcher = ({
|
|
|
484
557
|
}) => {
|
|
485
558
|
const [orgs, setOrgs] = useState4([]);
|
|
486
559
|
const [loading, setLoading] = useState4(true);
|
|
487
|
-
|
|
560
|
+
useEffect2(() => {
|
|
488
561
|
const fetchOrgs = async () => {
|
|
489
562
|
const { data } = await client.organization.list({});
|
|
490
563
|
if (data) setOrgs(data);
|
|
@@ -576,7 +649,7 @@ var InviteMemberForm = ({
|
|
|
576
649
|
};
|
|
577
650
|
|
|
578
651
|
// src/frontend/components/ProfileEditor.tsx
|
|
579
|
-
import { useState as useState5, useEffect as
|
|
652
|
+
import { useState as useState5, useEffect as useEffect3 } from "react";
|
|
580
653
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
581
654
|
var ProfileEditor = ({
|
|
582
655
|
client = authClient,
|
|
@@ -589,7 +662,7 @@ var ProfileEditor = ({
|
|
|
589
662
|
const [name, setName] = useState5(defaultName);
|
|
590
663
|
const [image, setImage] = useState5(defaultImage);
|
|
591
664
|
const [loading, setLoading] = useState5(false);
|
|
592
|
-
|
|
665
|
+
useEffect3(() => {
|
|
593
666
|
if (defaultName) setName(defaultName);
|
|
594
667
|
if (defaultImage) setImage(defaultImage);
|
|
595
668
|
}, [defaultName, defaultImage]);
|
|
@@ -694,7 +767,11 @@ var PasswordChanger = ({
|
|
|
694
767
|
setConfirmPassword("");
|
|
695
768
|
onSuccess?.(res?.data);
|
|
696
769
|
} catch (err) {
|
|
697
|
-
|
|
770
|
+
if (err?.code === "CREDENTIAL_ACCOUNT_NOT_FOUND" || err?.message?.includes("Credential account not found")) {
|
|
771
|
+
onError?.("You are logged in via a social provider (e.g. GitHub, Google) and do not have a password set.");
|
|
772
|
+
} else {
|
|
773
|
+
onError?.(err.message || "Failed to change password");
|
|
774
|
+
}
|
|
698
775
|
} finally {
|
|
699
776
|
setLoading(false);
|
|
700
777
|
}
|
|
@@ -628,7 +628,7 @@ declare const createClient: (baseUrl?: string) => {
|
|
|
628
628
|
sortDirection?: "asc" | "desc" | undefined;
|
|
629
629
|
filterField?: string | undefined;
|
|
630
630
|
filterValue?: string | number | boolean | undefined;
|
|
631
|
-
filterOperator?: "eq" | "ne" | "
|
|
631
|
+
filterOperator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | undefined;
|
|
632
632
|
organizationId?: string | undefined;
|
|
633
633
|
organizationSlug?: string | undefined;
|
|
634
634
|
}> & Record<string, any>, Record<string, any> | undefined>>(data_0?: better_auth.Prettify<{
|
|
@@ -639,7 +639,7 @@ declare const createClient: (baseUrl?: string) => {
|
|
|
639
639
|
sortDirection?: "asc" | "desc" | undefined;
|
|
640
640
|
filterField?: string | undefined;
|
|
641
641
|
filterValue?: string | number | boolean | undefined;
|
|
642
|
-
filterOperator?: "eq" | "ne" | "
|
|
642
|
+
filterOperator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | undefined;
|
|
643
643
|
organizationId?: string | undefined;
|
|
644
644
|
organizationSlug?: string | undefined;
|
|
645
645
|
} | undefined;
|
|
@@ -746,7 +746,7 @@ declare const createClient: (baseUrl?: string) => {
|
|
|
746
746
|
} & {
|
|
747
747
|
signIn: {
|
|
748
748
|
social: <FetchOptions extends better_auth.ClientFetchOption<Partial<{
|
|
749
|
-
provider: (string & {}) | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "
|
|
749
|
+
provider: (string & {}) | "linear" | "huggingface" | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "vercel";
|
|
750
750
|
callbackURL?: string | undefined;
|
|
751
751
|
newUserCallbackURL?: string | undefined;
|
|
752
752
|
errorCallbackURL?: string | undefined;
|
|
@@ -763,7 +763,7 @@ declare const createClient: (baseUrl?: string) => {
|
|
|
763
763
|
loginHint?: string | undefined;
|
|
764
764
|
additionalData?: Record<string, any> | undefined;
|
|
765
765
|
}> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: better_auth.Prettify<{
|
|
766
|
-
provider: (string & {}) | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "
|
|
766
|
+
provider: (string & {}) | "linear" | "huggingface" | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "vercel";
|
|
767
767
|
callbackURL?: string | undefined;
|
|
768
768
|
newUserCallbackURL?: string | undefined;
|
|
769
769
|
errorCallbackURL?: string | undefined;
|
|
@@ -2205,7 +2205,7 @@ declare const authClient: {
|
|
|
2205
2205
|
sortDirection?: "asc" | "desc" | undefined;
|
|
2206
2206
|
filterField?: string | undefined;
|
|
2207
2207
|
filterValue?: string | number | boolean | undefined;
|
|
2208
|
-
filterOperator?: "eq" | "ne" | "
|
|
2208
|
+
filterOperator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | undefined;
|
|
2209
2209
|
organizationId?: string | undefined;
|
|
2210
2210
|
organizationSlug?: string | undefined;
|
|
2211
2211
|
}> & Record<string, any>, Record<string, any> | undefined>>(data_0?: better_auth.Prettify<{
|
|
@@ -2216,7 +2216,7 @@ declare const authClient: {
|
|
|
2216
2216
|
sortDirection?: "asc" | "desc" | undefined;
|
|
2217
2217
|
filterField?: string | undefined;
|
|
2218
2218
|
filterValue?: string | number | boolean | undefined;
|
|
2219
|
-
filterOperator?: "eq" | "ne" | "
|
|
2219
|
+
filterOperator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | undefined;
|
|
2220
2220
|
organizationId?: string | undefined;
|
|
2221
2221
|
organizationSlug?: string | undefined;
|
|
2222
2222
|
} | undefined;
|
|
@@ -2323,7 +2323,7 @@ declare const authClient: {
|
|
|
2323
2323
|
} & {
|
|
2324
2324
|
signIn: {
|
|
2325
2325
|
social: <FetchOptions extends better_auth.ClientFetchOption<Partial<{
|
|
2326
|
-
provider: (string & {}) | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "
|
|
2326
|
+
provider: (string & {}) | "linear" | "huggingface" | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "vercel";
|
|
2327
2327
|
callbackURL?: string | undefined;
|
|
2328
2328
|
newUserCallbackURL?: string | undefined;
|
|
2329
2329
|
errorCallbackURL?: string | undefined;
|
|
@@ -2340,7 +2340,7 @@ declare const authClient: {
|
|
|
2340
2340
|
loginHint?: string | undefined;
|
|
2341
2341
|
additionalData?: Record<string, any> | undefined;
|
|
2342
2342
|
}> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: better_auth.Prettify<{
|
|
2343
|
-
provider: (string & {}) | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "
|
|
2343
|
+
provider: (string & {}) | "linear" | "huggingface" | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "vercel";
|
|
2344
2344
|
callbackURL?: string | undefined;
|
|
2345
2345
|
newUserCallbackURL?: string | undefined;
|
|
2346
2346
|
errorCallbackURL?: string | undefined;
|
package/dist/frontend/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ interface AuthFormProps {
|
|
|
24
24
|
lockEmail?: boolean;
|
|
25
25
|
/** URL for the forgot password page (shows link on login form if provided) */
|
|
26
26
|
forgotPasswordUrl?: string;
|
|
27
|
+
/** Cloudflare Turnstile site key. When set, shows Turnstile widget on signup. */
|
|
28
|
+
turnstileSiteKey?: string;
|
|
27
29
|
}
|
|
28
30
|
declare const AuthForm: React.FC<AuthFormProps>;
|
|
29
31
|
|
package/dist/frontend/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AuthConfig, authMiddleware, createAuth, createAuthApp, getInvitationLink, getSessionToken, schema } from './backend/index.js';
|
|
1
|
+
export { AuthConfig, EmailNormalizationConfig, TurnstileConfig, authMiddleware, createAuth, createAuthApp, getInvitationLink, getSessionToken, isGmailAddress, normalizeEmail, schema, verifyTurnstile } from './backend/index.js';
|
|
2
2
|
export { AuthForm, CreateOrganizationForm, ForgotPasswordForm, InviteMemberForm, OrganizationSwitcher, PasswordChanger, PasswordChangerProps, ProfileEditor, ProfileEditorProps, ResetPasswordForm } from './frontend/index.js';
|
|
3
3
|
export { authClient, createClient } from './frontend/client.js';
|
|
4
4
|
export * from 'better-auth';
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
createAuthApp,
|
|
6
6
|
getInvitationLink,
|
|
7
7
|
getSessionToken,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
isGmailAddress,
|
|
9
|
+
normalizeEmail,
|
|
10
|
+
schema_exports,
|
|
11
|
+
verifyTurnstile
|
|
12
|
+
} from "./chunk-H37QRFRH.js";
|
|
10
13
|
import {
|
|
11
14
|
AuthForm,
|
|
12
15
|
CreateOrganizationForm,
|
|
@@ -16,7 +19,7 @@ import {
|
|
|
16
19
|
PasswordChanger,
|
|
17
20
|
ProfileEditor,
|
|
18
21
|
ResetPasswordForm
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-XL5CRGGU.js";
|
|
20
23
|
import {
|
|
21
24
|
authClient,
|
|
22
25
|
createClient
|
|
@@ -39,5 +42,8 @@ export {
|
|
|
39
42
|
createClient,
|
|
40
43
|
getInvitationLink,
|
|
41
44
|
getSessionToken,
|
|
42
|
-
|
|
45
|
+
isGmailAddress,
|
|
46
|
+
normalizeEmail,
|
|
47
|
+
schema_exports as schema,
|
|
48
|
+
verifyTurnstile
|
|
43
49
|
};
|
package/dist/styles.css
CHANGED
|
@@ -346,4 +346,27 @@ button[type="submit"]:disabled {
|
|
|
346
346
|
flex-direction: column;
|
|
347
347
|
justify-content: center;
|
|
348
348
|
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Turnstile Widget */
|
|
352
|
+
.ca-turnstile {
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: column;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: 0.5rem;
|
|
357
|
+
margin: 0.5rem 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.ca-turnstile-hint {
|
|
361
|
+
font-size: 0.8125rem;
|
|
362
|
+
color: #6b7280;
|
|
363
|
+
text-align: center;
|
|
364
|
+
margin: 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Locked Input */
|
|
368
|
+
.ca-input-locked {
|
|
369
|
+
background-color: #f3f4f6;
|
|
370
|
+
color: #6b7280;
|
|
371
|
+
cursor: not-allowed;
|
|
349
372
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentgrowth/content-auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Better Auth wrapper with UI components for Cloudflare Workers & Pages",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Better Auth wrapper with UI components for Cloudflare Workers & Pages. Includes Turnstile bot protection and email normalization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -51,7 +51,13 @@
|
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": ">=18.2.0",
|
|
54
|
-
"react-dom": ">=18.2.0"
|
|
54
|
+
"react-dom": ">=18.2.0",
|
|
55
|
+
"@marsidev/react-turnstile": ">=0.5.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@marsidev/react-turnstile": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
55
61
|
},
|
|
56
62
|
"dependencies": {
|
|
57
63
|
"better-auth": "^1.4.9",
|