@contentgrowth/content-auth 0.2.9 → 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-EU3UKKTM.js → chunk-XL5CRGGU.js} +85 -40
- 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]);
|
|
@@ -661,7 +734,7 @@ var ProfileEditor = ({
|
|
|
661
734
|
};
|
|
662
735
|
|
|
663
736
|
// src/frontend/components/PasswordChanger.tsx
|
|
664
|
-
import
|
|
737
|
+
import { useState as useState6 } from "react";
|
|
665
738
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
666
739
|
var PasswordChanger = ({
|
|
667
740
|
client = authClient,
|
|
@@ -674,26 +747,6 @@ var PasswordChanger = ({
|
|
|
674
747
|
const [newPassword, setNewPassword] = useState6("");
|
|
675
748
|
const [confirmPassword, setConfirmPassword] = useState6("");
|
|
676
749
|
const [loading, setLoading] = useState6(false);
|
|
677
|
-
const [canChangePassword, setCanChangePassword] = useState6(true);
|
|
678
|
-
const [authMessage, setAuthMessage] = useState6(null);
|
|
679
|
-
React6.useEffect(() => {
|
|
680
|
-
const checkAccounts = async () => {
|
|
681
|
-
try {
|
|
682
|
-
const accounts = await client.listAccounts();
|
|
683
|
-
if (accounts?.data) {
|
|
684
|
-
const hasCredential = accounts.data.some((acc) => acc.providerId === "credential");
|
|
685
|
-
if (!hasCredential) {
|
|
686
|
-
setCanChangePassword(false);
|
|
687
|
-
const providers = accounts.data.map((acc) => acc.providerId).join(", ");
|
|
688
|
-
setAuthMessage(`You are signed in via ${providers}. You cannot change your password here because you don't have a password set.`);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
} catch (e) {
|
|
692
|
-
console.warn("Failed to list accounts", e);
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
checkAccounts();
|
|
696
|
-
}, [client]);
|
|
697
750
|
const handleSubmit = async (e) => {
|
|
698
751
|
e.preventDefault();
|
|
699
752
|
if (newPassword !== confirmPassword) {
|
|
@@ -715,9 +768,7 @@ var PasswordChanger = ({
|
|
|
715
768
|
onSuccess?.(res?.data);
|
|
716
769
|
} catch (err) {
|
|
717
770
|
if (err?.code === "CREDENTIAL_ACCOUNT_NOT_FOUND" || err?.message?.includes("Credential account not found")) {
|
|
718
|
-
|
|
719
|
-
setAuthMessage("You are logged in via a social provider and do not have a password set.");
|
|
720
|
-
onError?.("Password change unavailable for social logins.");
|
|
771
|
+
onError?.("You are logged in via a social provider (e.g. GitHub, Google) and do not have a password set.");
|
|
721
772
|
} else {
|
|
722
773
|
onError?.(err.message || "Failed to change password");
|
|
723
774
|
}
|
|
@@ -725,12 +776,6 @@ var PasswordChanger = ({
|
|
|
725
776
|
setLoading(false);
|
|
726
777
|
}
|
|
727
778
|
};
|
|
728
|
-
if (!canChangePassword) {
|
|
729
|
-
return /* @__PURE__ */ jsx6("div", { className: `ca-form ${className || ""}`, children: /* @__PURE__ */ jsxs6("div", { className: "ca-info-message", style: { textAlign: "center", padding: "2rem", background: "#f9fafb", borderRadius: "8px", border: "1px solid #e5e7eb" }, children: [
|
|
730
|
-
/* @__PURE__ */ jsx6("p", { style: { color: "#6b7280", marginBottom: "0.5rem" }, children: "Password Change Unavailable" }),
|
|
731
|
-
/* @__PURE__ */ jsx6("p", { style: { color: "#374151", fontSize: "0.95rem" }, children: authMessage || "Your account is managed by a third-party provider." })
|
|
732
|
-
] }) });
|
|
733
|
-
}
|
|
734
779
|
return /* @__PURE__ */ jsxs6("form", { onSubmit: handleSubmit, className: `ca-form ${className || ""}`, children: [
|
|
735
780
|
/* @__PURE__ */ jsxs6("div", { className: "ca-input-group", children: [
|
|
736
781
|
/* @__PURE__ */ jsx6("label", { className: "ca-label", htmlFor: "current-password", children: "Current Password" }),
|
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",
|