@mindfulauth/core 2.0.0-beta.9 → 3.0.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.
Files changed (47) hide show
  1. package/dist/authScripts/ForgotPasswordScript.astro +64 -0
  2. package/dist/authScripts/LoginScript.astro +209 -0
  3. package/dist/authScripts/MagicLoginScript.astro +62 -0
  4. package/dist/authScripts/MagicRegisterScript.astro +73 -0
  5. package/dist/authScripts/MainScript.astro +236 -0
  6. package/dist/authScripts/RegisterPasswordScript.astro +118 -0
  7. package/dist/authScripts/ResendVerificationScript.astro +51 -0
  8. package/dist/authScripts/ResetPasswordScript.astro +155 -0
  9. package/dist/authScripts/SecurityScript.astro +449 -0
  10. package/dist/authScripts/TurnstileInit.astro +112 -0
  11. package/dist/authScripts/VerifyEmailScript.astro +72 -0
  12. package/dist/authScripts/VerifyMagicLinkScript.astro +195 -0
  13. package/dist/authScripts/index.d.ts +13 -0
  14. package/dist/authScripts/index.d.ts.map +1 -0
  15. package/dist/authScripts/index.js +15 -0
  16. package/dist/components/MAuthEmailInput.astro +12 -0
  17. package/dist/components/MAuthForm.astro +10 -0
  18. package/dist/components/MAuthMessage.astro +8 -0
  19. package/dist/components/MAuthNameInput.astro +11 -0
  20. package/dist/components/MAuthPasswordChangePending.astro +8 -0
  21. package/dist/components/MAuthPasswordInput.astro +12 -0
  22. package/dist/components/MAuthSubmitButton.astro +8 -0
  23. package/dist/components/MAuthTurnstile.astro +11 -0
  24. package/dist/components/MAuthTwoFACodeInput.astro +13 -0
  25. package/dist/components/MAuthTwoFASection.astro +11 -0
  26. package/dist/components/index.d.ts +11 -0
  27. package/dist/components/index.d.ts.map +1 -0
  28. package/dist/components/index.js +13 -0
  29. package/dist/core/auth-handler.d.ts.map +1 -1
  30. package/dist/core/auth-handler.js +2 -6
  31. package/dist/core/auth.d.ts +1 -1
  32. package/dist/core/auth.d.ts.map +1 -1
  33. package/dist/core/auth.js +1 -1
  34. package/dist/core/config.d.ts +1 -2
  35. package/dist/core/config.d.ts.map +1 -1
  36. package/dist/core/config.js +6 -5
  37. package/dist/core/csp.d.ts +2 -2
  38. package/dist/core/csp.js +3 -3
  39. package/dist/core/index.d.ts +5 -7
  40. package/dist/core/index.d.ts.map +1 -1
  41. package/dist/core/index.js +11 -9
  42. package/dist/core/middleware.js +3 -3
  43. package/dist/core/types.d.ts +0 -5
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/layouts/MAuthProtected.astro +56 -0
  46. package/dist/layouts/MAuthPublic.astro +68 -0
  47. package/package.json +17 -12
@@ -0,0 +1,449 @@
1
+ ---
2
+ // Mindful Auth - Security Settings Script Component
3
+ // Provides: Change password, 2FA management, recovery codes, add auth method
4
+ ---
5
+ <script is:inline>
6
+ // Security Settings Script - Astro Optimized
7
+ // Combines: Change Password + 2FA Management + Add Authentication Method
8
+
9
+ // ============================================================================
10
+ // RECORDID EXTRACTION HELPER
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Extracts the recordId from the URL path (/{memberid}/...).
15
+ * The memberid is the first path segment, already validated server-side by middleware.
16
+ * @returns {string|undefined} The recordId if found, undefined otherwise
17
+ */
18
+ function getRecordId() {
19
+ return window.location.pathname.split('/').filter(Boolean)[0] || undefined;
20
+ }
21
+
22
+ // ============================================================================
23
+ // CHANGE PASSWORD
24
+ // ============================================================================
25
+
26
+ async function handleChangePasswordSubmit(event) {
27
+ event.preventDefault();
28
+ const form = event.target;
29
+ const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
30
+ const newPasswordEl = form.querySelector('[data-mindfulauth-field="new-password"]');
31
+ const confirmPasswordEl = form.querySelector('[data-mindfulauth-field="confirm-password"]');
32
+ const twoFACodeEl = form.querySelector('[data-mindfulauth-field="twofa-code"]');
33
+ const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-container"]');
34
+ const submitBtn = form.querySelector('button[type="submit"]');
35
+
36
+ messageEl.textContent = '';
37
+ const newPassword = newPasswordEl.value;
38
+ const confirmPassword = confirmPasswordEl.value;
39
+
40
+ if (newPassword !== confirmPassword) {
41
+ messageEl.textContent = 'Passwords do not match.';
42
+ return;
43
+ }
44
+
45
+ const recordid = getRecordId();
46
+
47
+ if (!recordid) {
48
+ messageEl.textContent = 'Error: Could not identify user. Please log in again.';
49
+ return;
50
+ }
51
+
52
+ messageEl.textContent = 'Validating password...';
53
+
54
+ try {
55
+ // First validate password against policy
56
+ const passwordValidateResponse = await window.apiFetch('/auth/validate-password', {
57
+ body: JSON.stringify({ password: newPassword }),
58
+ });
59
+
60
+ const passwordValidateResult = await passwordValidateResponse.json();
61
+ if (!passwordValidateResult.success) {
62
+ throw new Error(passwordValidateResult.message);
63
+ }
64
+
65
+ // Check if user has 2FA enabled (use cache)
66
+ messageEl.textContent = 'Checking security settings...';
67
+ const statusResult = await window.get2FAStatus();
68
+
69
+ // If 2FA is enabled and code not provided yet, show the input and wait
70
+ if (statusResult.isEnabled) {
71
+ if (!twoFACodeEl || !twoFACodeEl.value) {
72
+ // Show the 2FA input field
73
+ if (twoFAContainer) {
74
+ twoFAContainer.removeAttribute('hidden');
75
+ twoFAContainer.classList && twoFAContainer.classList.remove('hidden');
76
+ twoFAContainer.style.display = 'block';
77
+ }
78
+ if (twoFACodeEl) {
79
+ twoFACodeEl.focus();
80
+ }
81
+ messageEl.textContent = '2FA code is required for password change.';
82
+ if (submitBtn) {
83
+ submitBtn.textContent = 'Update Password with 2FA';
84
+ }
85
+ return;
86
+ }
87
+
88
+ const twoFACode = twoFACodeEl.value;
89
+ if (twoFACode.length !== 6) {
90
+ messageEl.textContent = 'Please enter a valid 6-digit 2FA code.';
91
+ return;
92
+ }
93
+ }
94
+
95
+ messageEl.textContent = 'Updating password...';
96
+
97
+ const requestBody = { recordid, newPassword };
98
+ if (statusResult.isEnabled && twoFACodeEl && twoFACodeEl.value) {
99
+ requestBody.twoFACode = twoFACodeEl.value;
100
+ }
101
+
102
+ const response = await window.apiFetch('/auth/change-password', {
103
+ body: JSON.stringify(requestBody),
104
+ });
105
+
106
+ const result = await response.json();
107
+ if (result.success) {
108
+ messageEl.textContent = 'Password updated successfully! Redirecting to login...';
109
+ form.reset();
110
+
111
+ // Password change invalidates all sessions for security
112
+ setTimeout(() => {
113
+ window.location.assign('/login');
114
+ }, 2000);
115
+
116
+ } else {
117
+ throw new Error(result.message || 'An unknown error occurred.');
118
+ }
119
+ } catch (error) {
120
+ messageEl.textContent = `Error: ${error.message}`;
121
+ }
122
+ }
123
+
124
+ function initChangePassword() {
125
+ const form = document.querySelector('[data-mindfulauth-form="change-password"]');
126
+ if (!form) return;
127
+
128
+ // Initially hide 2FA field - it will be shown during submit if needed
129
+ const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-container"]');
130
+ if (twoFAContainer) {
131
+ twoFAContainer.style.display = 'none';
132
+ }
133
+
134
+ form.addEventListener('submit', handleChangePasswordSubmit);
135
+ }
136
+
137
+ // ============================================================================
138
+ // 2FA MANAGEMENT
139
+ // ============================================================================
140
+
141
+ function init2FA() {
142
+ const enableBtn = document.querySelector('[data-mindfulauth-field="enable-btn"]');
143
+ if (!enableBtn) return;
144
+
145
+ const messageEl = document.querySelector('[data-mindfulauth-field="twofa-message"]');
146
+ const setupDiv = document.querySelector('[data-mindfulauth-field="setup"]');
147
+ const qrCodeContainer = document.querySelector('[data-mindfulauth-field="qr-code-container"]');
148
+ const verifyForm = document.querySelector('[data-mindfulauth-form="verify-2fa"]');
149
+ const tokenInput = document.querySelector('[data-mindfulauth-field="token"]');
150
+ const recoveryContainer = document.querySelector('[data-mindfulauth-field="recovery-codes-container"]');
151
+ const recoveryList = document.querySelector('[data-mindfulauth-field="recovery-codes-list"]');
152
+ const copyBtn = document.querySelector('[data-mindfulauth-field="copy-codes-btn"]');
153
+ const copyMessageEl = document.querySelector('[data-mindfulauth-field="copy-message"]');
154
+ const disableForm = document.querySelector('[data-mindfulauth-form="disable-2fa"]');
155
+ const disablePasswordInput = document.querySelector('[data-mindfulauth-field="disable-password"]');
156
+ const disableMessageEl = document.querySelector('[data-mindfulauth-field="disable-message"]');
157
+ const disableBtn = document.querySelector('[data-mindfulauth-field="disable-btn"]');
158
+ const enableSection = document.querySelector('[data-mindfulauth-section="enable"]');
159
+ const disableSection = document.querySelector('[data-mindfulauth-section="disable"]');
160
+ const recordid = getRecordId();
161
+
162
+ // --- START 2FA Setup ---
163
+ async function handleEnable2FA() {
164
+ if (!recordid) {
165
+ messageEl.textContent = "Session not found. Please log in again.";
166
+ return;
167
+ }
168
+ if (!qrCodeContainer || !setupDiv) {
169
+ messageEl.textContent = 'Unable to start 2FA (missing UI elements).';
170
+ return;
171
+ }
172
+
173
+ enableBtn.disabled = true;
174
+ enableBtn.style.display = 'none';
175
+ setupDiv.removeAttribute('hidden');
176
+ setupDiv.classList && setupDiv.classList.remove('hidden');
177
+ setupDiv.style.display = 'flex';
178
+ qrCodeContainer.replaceChildren();
179
+
180
+ messageEl.textContent = 'Generating QR code...';
181
+ try {
182
+ const response = await window.apiFetch('/auth/setup-2fa', { body: JSON.stringify({ recordid }) });
183
+ const result = await response.json();
184
+ if (result.success) {
185
+ qrCodeContainer.replaceChildren();
186
+
187
+ const img = document.createElement('img');
188
+ img.src = result.qrCodeDataUrl;
189
+ img.width = 256;
190
+ img.alt = 'QR code for authenticator app';
191
+ qrCodeContainer.appendChild(img);
192
+
193
+ messageEl.textContent = 'Scan the QR code with your authenticator app and enter the code below.';
194
+ } else {
195
+ throw new Error(result.message || 'Failed to start 2FA setup.');
196
+ }
197
+ } catch (error) {
198
+ messageEl.textContent = `Error: ${error.message}`;
199
+ enableBtn.disabled = false;
200
+ enableBtn.style.display = '';
201
+ setupDiv.style.display = 'none';
202
+ setupDiv.classList && setupDiv.classList.add('hidden');
203
+ setupDiv.setAttribute('hidden', '');
204
+ }
205
+ }
206
+
207
+ enableBtn.addEventListener('click', handleEnable2FA);
208
+
209
+ // --- VERIFY and ENABLE 2FA ---
210
+ if (verifyForm) {
211
+ verifyForm.addEventListener('submit', async (event) => {
212
+ event.preventDefault();
213
+ const token = tokenInput.value;
214
+ if (!recordid || !token || token.length !== 6) {
215
+ messageEl.textContent = "Please enter a valid 6-digit code.";
216
+ return;
217
+ }
218
+ messageEl.textContent = 'Verifying code...';
219
+
220
+ try {
221
+ const response = await window.apiFetch('/auth/verify-2fa-setup', {
222
+ body: JSON.stringify({ recordid, token })
223
+ });
224
+
225
+ const result = await response.json();
226
+ if (result.success) {
227
+ setupDiv.style.display = 'none';
228
+ messageEl.textContent = result.message;
229
+
230
+ if (result.recoveryCodes && result.recoveryCodes.length > 0) {
231
+ recoveryList.replaceChildren();
232
+ result.recoveryCodes.forEach(code => {
233
+ const li = document.createElement('li');
234
+ li.textContent = code;
235
+ recoveryList.appendChild(li);
236
+ });
237
+ recoveryContainer.removeAttribute('hidden');
238
+ recoveryContainer.style.display = 'flex';
239
+ }
240
+
241
+ // Invalidate cached 2FA status
242
+ if (typeof window !== 'undefined') {
243
+ window.__2faStatus = undefined;
244
+ checkInitial2FAStatus();
245
+ }
246
+ } else {
247
+ throw new Error(result.message || 'Verification failed.');
248
+ }
249
+ } catch (error) {
250
+ messageEl.textContent = `Error: ${error.message}`;
251
+ }
252
+ });
253
+ }
254
+
255
+ // --- DISABLE 2FA ---
256
+ if (disableForm) {
257
+ disableForm.addEventListener('submit', async (event) => {
258
+ event.preventDefault();
259
+ const password = disablePasswordInput.value;
260
+ if (!recordid) {
261
+ disableMessageEl.textContent = 'Session not found. Please log in again.';
262
+ return;
263
+ }
264
+ if (!password) {
265
+ disableMessageEl.textContent = 'Password is required.';
266
+ return;
267
+ }
268
+
269
+ disableBtn.disabled = true;
270
+ disableMessageEl.textContent = 'Disabling...';
271
+
272
+ try {
273
+ const response = await window.apiFetch('/auth/disable-2fa', {
274
+ body: JSON.stringify({ recordid, password }),
275
+ });
276
+
277
+ const result = await response.json();
278
+ disableMessageEl.textContent = result.message;
279
+
280
+ if (result.success) {
281
+ if (typeof window !== 'undefined') {
282
+ window.__2faStatus = undefined;
283
+ }
284
+
285
+ disableBtn.disabled = true;
286
+ setTimeout(() => window.location.reload(), 2000);
287
+ } else {
288
+ disableBtn.disabled = false;
289
+ }
290
+ } catch (error) {
291
+ disableMessageEl.textContent = `Error: ${error.message}`;
292
+ disableBtn.disabled = false;
293
+ }
294
+ });
295
+ }
296
+
297
+ // --- CHECK 2FA STATUS ON LOAD ---
298
+ async function checkInitial2FAStatus() {
299
+ try {
300
+ const result = await window.get2FAStatus();
301
+ if (result.isEnabled) {
302
+ if (enableSection) enableSection.style.display = 'none';
303
+ if (disableSection) disableSection.style.display = 'flex';
304
+ } else {
305
+ if (enableSection) enableSection.style.display = 'flex';
306
+ if (disableSection) disableSection.style.display = 'none';
307
+ }
308
+ } catch (error) {
309
+ if (enableSection) enableSection.style.display = 'none';
310
+ if (disableSection) disableSection.style.display = 'none';
311
+ }
312
+ }
313
+
314
+ checkInitial2FAStatus();
315
+
316
+ // --- COPY RECOVERY CODES ---
317
+ if (copyBtn) {
318
+ copyBtn.addEventListener('click', function() {
319
+ const codes = Array.from(recoveryList.querySelectorAll('li')).map(li => li.textContent).join('\n');
320
+ navigator.clipboard.writeText(codes).then(() => {
321
+ copyMessageEl.textContent = 'Copied to clipboard!';
322
+ setTimeout(() => copyMessageEl.textContent = '', 2000);
323
+ }).catch(err => {
324
+ copyMessageEl.textContent = 'Failed to copy.';
325
+ });
326
+ });
327
+ }
328
+ }
329
+
330
+ // ============================================================================
331
+ // ADD AUTHENTICATION METHOD
332
+ // ============================================================================
333
+
334
+ async function handleAddAuthMethodSubmit(event) {
335
+ event.preventDefault();
336
+ const form = event.target;
337
+ const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
338
+ const methodEl = form.querySelector('[data-mindfulauth-field="method"]');
339
+ const passwordEl = form.querySelector('[data-mindfulauth-field="new-password"]');
340
+ const submitBtn = form.querySelector('button[type="submit"]');
341
+
342
+ const selectedMethod = methodEl.querySelector('input[name="method"]:checked')?.value;
343
+
344
+ if (!selectedMethod) {
345
+ messageEl.textContent = 'Please select an authentication method.';
346
+ return;
347
+ }
348
+
349
+ if (selectedMethod === 'Password') {
350
+ if (!passwordEl.value || passwordEl.value.length < 8) {
351
+ messageEl.textContent = 'Password must be at least 8 characters.';
352
+ return;
353
+ }
354
+ }
355
+
356
+ messageEl.textContent = 'Adding authentication method...';
357
+ if (submitBtn) {
358
+ submitBtn.disabled = true;
359
+ }
360
+
361
+ try {
362
+ const body = {
363
+ method: selectedMethod
364
+ };
365
+
366
+ if (selectedMethod === 'Password') {
367
+ body.password = passwordEl.value;
368
+ }
369
+
370
+ const response = await window.apiFetch('/auth/add-authentication-method', {
371
+ body: JSON.stringify(body)
372
+ });
373
+
374
+ const result = await response.json();
375
+
376
+ if (result.success) {
377
+ messageEl.textContent = result.message || `${selectedMethod} authentication has been added to your account.`;
378
+ messageEl.style.color = 'green';
379
+
380
+ if (passwordEl) {
381
+ passwordEl.value = '';
382
+ }
383
+
384
+ setTimeout(() => {
385
+ event.target.style.display = 'none';
386
+ }, 2000);
387
+ } else {
388
+ throw new Error(result.message || 'Failed to add authentication method.');
389
+ }
390
+ } catch (error) {
391
+ messageEl.textContent = `Error: ${error.message}`;
392
+ messageEl.style.color = 'red';
393
+ } finally {
394
+ if (submitBtn) {
395
+ submitBtn.disabled = false;
396
+ }
397
+ }
398
+ }
399
+
400
+ function handleMethodChange(event) {
401
+ const passwordContainerEl = document.querySelector('[data-mindfulauth-field="password-container"]');
402
+
403
+ if (!passwordContainerEl) return;
404
+
405
+ if (event.target.value === 'Password') {
406
+ passwordContainerEl.removeAttribute('hidden');
407
+ if (passwordContainerEl.classList) {
408
+ passwordContainerEl.classList.remove('hidden');
409
+ }
410
+ passwordContainerEl.style.display = 'flex';
411
+ } else {
412
+ passwordContainerEl.style.display = 'none';
413
+ if (passwordContainerEl.classList) {
414
+ passwordContainerEl.classList.add('hidden');
415
+ }
416
+ passwordContainerEl.setAttribute('hidden', '');
417
+ }
418
+ }
419
+
420
+ function initAddAuthMethod() {
421
+ const form = document.querySelector('[data-mindfulauth-form="add-auth"]');
422
+ if (!form) return;
423
+
424
+ form.addEventListener('submit', handleAddAuthMethodSubmit);
425
+
426
+ // Bind radio change handlers
427
+ const radios = document.querySelectorAll('input[name="method"]');
428
+ radios.forEach(radio => {
429
+ radio.addEventListener('change', handleMethodChange);
430
+ });
431
+
432
+ // Set initial state
433
+ const checkedRadio = document.querySelector('input[name="method"]:checked');
434
+ if (checkedRadio) {
435
+ handleMethodChange({ target: checkedRadio });
436
+ }
437
+ }
438
+
439
+ // ============================================================================
440
+ // MAIN INITIALIZATION
441
+ // ============================================================================
442
+
443
+ document.addEventListener('DOMContentLoaded', function() {
444
+ // Initialize all security modules
445
+ initChangePassword();
446
+ init2FA();
447
+ initAddAuthMethod();
448
+ });
449
+ </script>
@@ -0,0 +1,112 @@
1
+ ---
2
+ // Mindful Auth - Turnstile Initialization Component
3
+ // Provides: Cloudflare Turnstile widget initialization and auto-config
4
+ ---
5
+ <script is:inline>
6
+ // Shared Cloudflare Turnstile initialization utilities - Astro Optimized
7
+ // Loaded only on pages that require bot protection (login, forgot password, reset password)
8
+
9
+ (function () {
10
+ window.initTurnstile = function initTurnstile(sitekey) {
11
+ if (!sitekey) {
12
+ return;
13
+ }
14
+
15
+ const container = document.querySelector('[data-mindfulauth-turnstile]');
16
+ if (container && window.turnstile) {
17
+ // Read customization from data attributes
18
+ const theme = container.getAttribute('data-theme') || 'light';
19
+ const size = container.getAttribute('data-size') || 'flexible';
20
+ const language = container.getAttribute('data-language') || 'auto';
21
+ const appearance = container.getAttribute('data-appearance') || 'always';
22
+
23
+ container.textContent = '';
24
+ try {
25
+ window.turnstile.render(container, {
26
+ sitekey: sitekey,
27
+ theme: theme,
28
+ size: size,
29
+ language: language,
30
+ appearance: appearance,
31
+ callback: function(token) {
32
+ // Find the hidden input field for the token (if it exists)
33
+ const form = container.closest('form') || document.querySelector('[data-mindfulauth-form]');
34
+ if (form) {
35
+ const tokenInput = form.querySelector('[name="cf-turnstile-response"]');
36
+ if (tokenInput) {
37
+ tokenInput.value = token;
38
+ }
39
+ }
40
+ },
41
+ errorCallback: function() {
42
+ // Create error message safely without innerHTML
43
+ var errorDiv = document.createElement('div');
44
+ errorDiv.style.cssText = 'padding: 10px; background: #fee; color: #c33; border: 1px solid #faa; border-radius: 4px;';
45
+ errorDiv.textContent = 'Bot protection widget failed to load. Please check your configuration.';
46
+ container.textContent = '';
47
+ container.appendChild(errorDiv);
48
+ }
49
+ });
50
+ } catch (error) {
51
+ // Create error message safely without innerHTML
52
+ var errorDiv = document.createElement('div');
53
+ errorDiv.style.cssText = 'padding: 10px; background: #fee; color: #c33; border: 1px solid #faa; border-radius: 4px;';
54
+ errorDiv.textContent = 'Bot protection widget failed to load. Please check your configuration.';
55
+ container.textContent = '';
56
+ container.appendChild(errorDiv);
57
+ }
58
+ }
59
+ };
60
+
61
+ window.ensureTurnstileReady = function ensureTurnstileReady(callback, attempts = 0) {
62
+ if (window.turnstile) {
63
+ callback();
64
+ } else if (attempts < 50) {
65
+ setTimeout(() => window.ensureTurnstileReady(callback, attempts + 1), 100);
66
+ }
67
+ };
68
+
69
+ // Auto-initialize Turnstile on page load if container exists
70
+ // Fetches tenant config to get the correct sitekey for this hostname
71
+ window.autoInitTurnstile = async function autoInitTurnstile() {
72
+ const container = document.querySelector('[data-mindfulauth-turnstile]');
73
+ if (!container) {
74
+ return;
75
+ }
76
+
77
+ // Skip API call on localhost to prevent production logs pollution
78
+ const hostname = window.location.hostname;
79
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local')) {
80
+ console.log('[Turnstile] Skipping initialization on localhost');
81
+ return;
82
+ }
83
+
84
+ try {
85
+ const configResponse = await fetch(`/auth/get-tenant-config`, {
86
+ headers: {
87
+ 'X-Tenant-Domain': window.location.hostname
88
+ }
89
+ });
90
+
91
+ if (!configResponse.ok) {
92
+ console.error('[Turnstile] Failed to fetch tenant config: HTTP ' + configResponse.status);
93
+ return;
94
+ }
95
+
96
+ const config = await configResponse.json();
97
+ if (!config.success || !config.turnstileSitekey) {
98
+ console.error('[Turnstile] Invalid tenant config response:', config);
99
+ return;
100
+ }
101
+ window.ensureTurnstileReady(() => {
102
+ window.initTurnstile(config.turnstileSitekey);
103
+ });
104
+ } catch (error) {
105
+ console.error('[Turnstile] Failed to initialize:', error instanceof Error ? error.message : String(error));
106
+ }
107
+ };
108
+
109
+ // Call auto-init when DOM is ready
110
+ document.addEventListener('DOMContentLoaded', window.autoInitTurnstile);
111
+ })();
112
+ </script>
@@ -0,0 +1,72 @@
1
+ ---
2
+ // Mindful Auth - Verify Email Script Component
3
+ // Provides: Email verification handling
4
+ ---
5
+ <script is:inline>
6
+ // Verify email script - Astro Optimized
7
+ function getPathParams() {
8
+ const pathParts = window.location.pathname.split('/');
9
+ return {
10
+ recordid: pathParts[2],
11
+ token: pathParts[3]
12
+ };
13
+ }
14
+
15
+ async function verifyEmail() {
16
+ const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
17
+ const { recordid, token } = getPathParams();
18
+
19
+ // Skip API call on localhost to prevent production logs pollution
20
+ const hostname = window.location.hostname;
21
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local')) {
22
+ if (messageEl) {
23
+ messageEl.textContent = 'Email verification skipped on localhost.';
24
+ }
25
+ console.log('[Verify Email] Skipping API call on localhost');
26
+ return;
27
+ }
28
+
29
+ if (!recordid || !token) {
30
+ if (messageEl) {
31
+ messageEl.textContent = "Invalid verification link. Please check your email for the correct link.";
32
+ }
33
+ return;
34
+ }
35
+
36
+ if (messageEl) {
37
+ messageEl.textContent = "Verifying your email...";
38
+ }
39
+
40
+ try {
41
+ const response = await window.apiFetch(`/auth/verify-email/${recordid}/${token}`, {
42
+ method: 'POST'
43
+ });
44
+
45
+ const result = await response.json();
46
+
47
+ if (messageEl) {
48
+ messageEl.textContent = result.message || (result.success
49
+ ? 'Email verified successfully! You can now log in to your account.'
50
+ : 'Verification failed. Please try again or contact support.');
51
+ }
52
+
53
+ // Show login button/link if verification succeeded
54
+ if (result.success) {
55
+ const loginLinkEl = document.querySelector('[data-mindfulauth-field="login-link"]');
56
+ if (loginLinkEl) {
57
+ loginLinkEl.style.display = 'block';
58
+ }
59
+ }
60
+ } catch (error) {
61
+ if (messageEl) {
62
+ messageEl.textContent = 'Error: ' + (error.message || 'Verification failed. Please try again.');
63
+ }
64
+ }
65
+ }
66
+
67
+ // --- MAIN EXECUTION ---
68
+ document.addEventListener('DOMContentLoaded', function() {
69
+ // Auto-verify on page load
70
+ verifyEmail();
71
+ });
72
+ </script>