@mindfulauth/core 2.0.0-beta.5 → 2.0.0-beta.6
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/astro/ForgotPasswordScript.astro +64 -0
- package/dist/astro/LoginScript.astro +209 -0
- package/dist/astro/MagicLoginScript.astro +62 -0
- package/dist/astro/MagicRegisterScript.astro +73 -0
- package/dist/astro/MainScript.astro +236 -0
- package/dist/astro/RegisterPasswordScript.astro +118 -0
- package/dist/astro/ResendVerificationScript.astro +51 -0
- package/dist/astro/ResetPasswordScript.astro +155 -0
- package/dist/astro/SecurityScript.astro +490 -0
- package/dist/astro/TurnstileInit.astro +112 -0
- package/dist/astro/VerifyEmailScript.astro +72 -0
- package/dist/astro/VerifyMagicLinkScript.astro +195 -0
- package/package.json +17 -12
- package/dist/auth-handler.d.ts +0 -5
- package/dist/auth-handler.d.ts.map +0 -1
- package/dist/auth-handler.js +0 -154
- package/dist/auth.d.ts +0 -9
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -56
- package/dist/config.d.ts +0 -49
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -95
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -13
- package/dist/middleware.d.ts +0 -2
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -108
- package/dist/security.d.ts +0 -5
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js +0 -31
- package/dist/types.d.ts +0 -22
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
|
@@ -0,0 +1,490 @@
|
|
|
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
|
+
// QRCODE DYNAMIC LOADING (bundled via qrcode npm package)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
// Capture CDN origin at load time for loading co-hosted libraries
|
|
14
|
+
const __cdnOrigin = (() => {
|
|
15
|
+
try {
|
|
16
|
+
if (document.currentScript && document.currentScript.src) {
|
|
17
|
+
return new URL(document.currentScript.src).origin;
|
|
18
|
+
}
|
|
19
|
+
} catch (_) {}
|
|
20
|
+
return '';
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dynamically loads the bundled QRCode library from the same CDN origin.
|
|
25
|
+
* Uses the qrcode npm package (bundled as IIFE via esbuild).
|
|
26
|
+
* Exposes QRCode.toCanvas(), QRCode.toDataURL(), QRCode.toString()
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
async function loadQRCodeLibrary() {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const script = document.createElement('script');
|
|
37
|
+
script.src = `${__cdnOrigin}/lib/qrcode.js`;
|
|
38
|
+
script.onload = () => resolve();
|
|
39
|
+
script.onerror = () => reject(new Error('Failed to load QR code library'));
|
|
40
|
+
document.head.appendChild(script);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// RECORDID EXTRACTION HELPER
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extracts recordid from window variable (injected server-side by backend)
|
|
50
|
+
* Backend MUST inject this variable for all implementations
|
|
51
|
+
* Architecture
|
|
52
|
+
* - All protected routes require /{memberid}/page URL structure
|
|
53
|
+
* - Backend extracts memberid from URL path and validates against session
|
|
54
|
+
* - Frontend receives memberid via window.MEMBERID injection
|
|
55
|
+
* @returns {string|null} The recordid if found, null otherwise
|
|
56
|
+
*/
|
|
57
|
+
function getRecordId() {
|
|
58
|
+
return window.MEMBERID || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// CHANGE PASSWORD
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
async function handleChangePasswordSubmit(event) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
const form = event.target;
|
|
68
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
69
|
+
const newPasswordEl = form.querySelector('[data-mindfulauth-field="new-password"]');
|
|
70
|
+
const confirmPasswordEl = form.querySelector('[data-mindfulauth-field="confirm-password"]');
|
|
71
|
+
const twoFACodeEl = form.querySelector('[data-mindfulauth-field="twofa-code"]');
|
|
72
|
+
const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-container"]');
|
|
73
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
74
|
+
|
|
75
|
+
messageEl.textContent = '';
|
|
76
|
+
const newPassword = newPasswordEl.value;
|
|
77
|
+
const confirmPassword = confirmPasswordEl.value;
|
|
78
|
+
|
|
79
|
+
if (newPassword !== confirmPassword) {
|
|
80
|
+
messageEl.textContent = 'Passwords do not match.';
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const recordid = getRecordId();
|
|
85
|
+
|
|
86
|
+
if (!recordid) {
|
|
87
|
+
messageEl.textContent = 'Error: Could not identify user. Please log in again.';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
messageEl.textContent = 'Validating password...';
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// First validate password against policy
|
|
95
|
+
const passwordValidateResponse = await window.apiFetch('/auth/validate-password', {
|
|
96
|
+
body: JSON.stringify({ password: newPassword }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const passwordValidateResult = await passwordValidateResponse.json();
|
|
100
|
+
if (!passwordValidateResult.success) {
|
|
101
|
+
throw new Error(passwordValidateResult.message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if user has 2FA enabled (use cache)
|
|
105
|
+
messageEl.textContent = 'Checking security settings...';
|
|
106
|
+
const statusResult = await window.get2FAStatus();
|
|
107
|
+
|
|
108
|
+
// If 2FA is enabled and code not provided yet, show the input and wait
|
|
109
|
+
if (statusResult.isEnabled) {
|
|
110
|
+
if (!twoFACodeEl || !twoFACodeEl.value) {
|
|
111
|
+
// Show the 2FA input field
|
|
112
|
+
if (twoFAContainer) {
|
|
113
|
+
twoFAContainer.removeAttribute('hidden');
|
|
114
|
+
twoFAContainer.classList && twoFAContainer.classList.remove('hidden');
|
|
115
|
+
twoFAContainer.style.display = 'block';
|
|
116
|
+
}
|
|
117
|
+
if (twoFACodeEl) {
|
|
118
|
+
twoFACodeEl.focus();
|
|
119
|
+
}
|
|
120
|
+
messageEl.textContent = '2FA code is required for password change.';
|
|
121
|
+
if (submitBtn) {
|
|
122
|
+
submitBtn.textContent = 'Update Password with 2FA';
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const twoFACode = twoFACodeEl.value;
|
|
128
|
+
if (twoFACode.length !== 6) {
|
|
129
|
+
messageEl.textContent = 'Please enter a valid 6-digit 2FA code.';
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
messageEl.textContent = 'Updating password...';
|
|
135
|
+
|
|
136
|
+
const requestBody = { recordid, newPassword };
|
|
137
|
+
if (statusResult.isEnabled && twoFACodeEl && twoFACodeEl.value) {
|
|
138
|
+
requestBody.twoFACode = twoFACodeEl.value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const response = await window.apiFetch('/auth/change-password', {
|
|
142
|
+
body: JSON.stringify(requestBody),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await response.json();
|
|
146
|
+
if (result.success) {
|
|
147
|
+
messageEl.textContent = 'Password updated successfully! Redirecting to login...';
|
|
148
|
+
form.reset();
|
|
149
|
+
|
|
150
|
+
// Password change invalidates all sessions for security
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
window.location.assign('/login');
|
|
153
|
+
}, 2000);
|
|
154
|
+
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(result.message || 'An unknown error occurred.');
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
messageEl.textContent = `Error: ${error.message}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function initChangePassword() {
|
|
164
|
+
const form = document.querySelector('[data-mindfulauth-form="change-password"]');
|
|
165
|
+
if (!form) return;
|
|
166
|
+
|
|
167
|
+
// Initially hide 2FA field - it will be shown during submit if needed
|
|
168
|
+
const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-container"]');
|
|
169
|
+
if (twoFAContainer) {
|
|
170
|
+
twoFAContainer.style.display = 'none';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
form.addEventListener('submit', handleChangePasswordSubmit);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// 2FA MANAGEMENT
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
function init2FA() {
|
|
181
|
+
const enableBtn = document.querySelector('[data-mindfulauth-field="enable-btn"]');
|
|
182
|
+
if (!enableBtn) return;
|
|
183
|
+
|
|
184
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="twofa-message"]');
|
|
185
|
+
const setupDiv = document.querySelector('[data-mindfulauth-field="setup"]');
|
|
186
|
+
const qrCodeContainer = document.querySelector('[data-mindfulauth-field="qr-code-container"]');
|
|
187
|
+
const verifyForm = document.querySelector('[data-mindfulauth-form="verify-2fa"]');
|
|
188
|
+
const tokenInput = document.querySelector('[data-mindfulauth-field="token"]');
|
|
189
|
+
const recoveryContainer = document.querySelector('[data-mindfulauth-field="recovery-codes-container"]');
|
|
190
|
+
const recoveryList = document.querySelector('[data-mindfulauth-field="recovery-codes-list"]');
|
|
191
|
+
const copyBtn = document.querySelector('[data-mindfulauth-field="copy-codes-btn"]');
|
|
192
|
+
const copyMessageEl = document.querySelector('[data-mindfulauth-field="copy-message"]');
|
|
193
|
+
const disableForm = document.querySelector('[data-mindfulauth-form="disable-2fa"]');
|
|
194
|
+
const disablePasswordInput = document.querySelector('[data-mindfulauth-field="disable-password"]');
|
|
195
|
+
const disableMessageEl = document.querySelector('[data-mindfulauth-field="disable-message"]');
|
|
196
|
+
const disableBtn = document.querySelector('[data-mindfulauth-field="disable-btn"]');
|
|
197
|
+
const enableSection = document.querySelector('[data-mindfulauth-section="enable"]');
|
|
198
|
+
const disableSection = document.querySelector('[data-mindfulauth-section="disable"]');
|
|
199
|
+
const recordid = getRecordId();
|
|
200
|
+
|
|
201
|
+
// --- START 2FA Setup ---
|
|
202
|
+
async function handleEnable2FA() {
|
|
203
|
+
if (!recordid) {
|
|
204
|
+
messageEl.textContent = "Session not found. Please log in again.";
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!qrCodeContainer || !setupDiv) {
|
|
208
|
+
messageEl.textContent = 'Unable to start 2FA (missing UI elements).';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
enableBtn.disabled = true;
|
|
213
|
+
enableBtn.style.display = 'none';
|
|
214
|
+
setupDiv.removeAttribute('hidden');
|
|
215
|
+
setupDiv.classList && setupDiv.classList.remove('hidden');
|
|
216
|
+
setupDiv.style.display = 'flex';
|
|
217
|
+
qrCodeContainer.innerHTML = '';
|
|
218
|
+
|
|
219
|
+
messageEl.textContent = 'Loading QR code generator...';
|
|
220
|
+
try {
|
|
221
|
+
// Load QRCode library dynamically
|
|
222
|
+
await loadQRCodeLibrary();
|
|
223
|
+
|
|
224
|
+
messageEl.textContent = 'Generating secret key...';
|
|
225
|
+
const response = await window.apiFetch('/auth/setup-2fa', { body: JSON.stringify({ recordid }) });
|
|
226
|
+
const result = await response.json();
|
|
227
|
+
if (result.success) {
|
|
228
|
+
qrCodeContainer.innerHTML = '';
|
|
229
|
+
|
|
230
|
+
const canvas = document.createElement('canvas');
|
|
231
|
+
await QRCode.toCanvas(canvas, result.otpauthUri, { width: 256, margin: 2 });
|
|
232
|
+
qrCodeContainer.appendChild(canvas);
|
|
233
|
+
|
|
234
|
+
messageEl.textContent = 'Scan the QR code with your authenticator app and enter the code below.';
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error(result.message || 'Failed to start 2FA setup.');
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
messageEl.textContent = `Error: ${error.message}`;
|
|
240
|
+
enableBtn.disabled = false;
|
|
241
|
+
enableBtn.style.display = '';
|
|
242
|
+
setupDiv.style.display = 'none';
|
|
243
|
+
setupDiv.classList && setupDiv.classList.add('hidden');
|
|
244
|
+
setupDiv.setAttribute('hidden', '');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
enableBtn.addEventListener('click', handleEnable2FA);
|
|
249
|
+
|
|
250
|
+
// --- VERIFY and ENABLE 2FA ---
|
|
251
|
+
if (verifyForm) {
|
|
252
|
+
verifyForm.addEventListener('submit', async (event) => {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
const token = tokenInput.value;
|
|
255
|
+
if (!recordid || !token || token.length !== 6) {
|
|
256
|
+
messageEl.textContent = "Please enter a valid 6-digit code.";
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
messageEl.textContent = 'Verifying code...';
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const response = await window.apiFetch('/auth/verify-2fa-setup', {
|
|
263
|
+
body: JSON.stringify({ recordid, token })
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const result = await response.json();
|
|
267
|
+
if (result.success) {
|
|
268
|
+
setupDiv.style.display = 'none';
|
|
269
|
+
messageEl.textContent = result.message;
|
|
270
|
+
|
|
271
|
+
if (result.recoveryCodes && result.recoveryCodes.length > 0) {
|
|
272
|
+
recoveryList.innerHTML = '';
|
|
273
|
+
result.recoveryCodes.forEach(code => {
|
|
274
|
+
const li = document.createElement('li');
|
|
275
|
+
li.textContent = code;
|
|
276
|
+
recoveryList.appendChild(li);
|
|
277
|
+
});
|
|
278
|
+
recoveryContainer.removeAttribute('hidden');
|
|
279
|
+
recoveryContainer.style.display = 'flex';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Invalidate cached 2FA status
|
|
283
|
+
if (typeof window !== 'undefined') {
|
|
284
|
+
window.__2faStatus = undefined;
|
|
285
|
+
checkInitial2FAStatus();
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
throw new Error(result.message || 'Verification failed.');
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
messageEl.textContent = `Error: ${error.message}`;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- DISABLE 2FA ---
|
|
297
|
+
if (disableForm) {
|
|
298
|
+
disableForm.addEventListener('submit', async (event) => {
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
const password = disablePasswordInput.value;
|
|
301
|
+
if (!recordid) {
|
|
302
|
+
disableMessageEl.textContent = 'Session not found. Please log in again.';
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!password) {
|
|
306
|
+
disableMessageEl.textContent = 'Password is required.';
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
disableBtn.disabled = true;
|
|
311
|
+
disableMessageEl.textContent = 'Disabling...';
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const response = await window.apiFetch('/auth/disable-2fa', {
|
|
315
|
+
body: JSON.stringify({ recordid, password }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await response.json();
|
|
319
|
+
disableMessageEl.textContent = result.message;
|
|
320
|
+
|
|
321
|
+
if (result.success) {
|
|
322
|
+
if (typeof window !== 'undefined') {
|
|
323
|
+
window.__2faStatus = undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
disableBtn.disabled = true;
|
|
327
|
+
setTimeout(() => window.location.reload(), 2000);
|
|
328
|
+
} else {
|
|
329
|
+
disableBtn.disabled = false;
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
disableMessageEl.textContent = `Error: ${error.message}`;
|
|
333
|
+
disableBtn.disabled = false;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- CHECK 2FA STATUS ON LOAD ---
|
|
339
|
+
async function checkInitial2FAStatus() {
|
|
340
|
+
try {
|
|
341
|
+
const result = await window.get2FAStatus();
|
|
342
|
+
if (result.isEnabled) {
|
|
343
|
+
if (enableSection) enableSection.style.display = 'none';
|
|
344
|
+
if (disableSection) disableSection.style.display = 'flex';
|
|
345
|
+
} else {
|
|
346
|
+
if (enableSection) enableSection.style.display = 'flex';
|
|
347
|
+
if (disableSection) disableSection.style.display = 'none';
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (enableSection) enableSection.style.display = 'none';
|
|
351
|
+
if (disableSection) disableSection.style.display = 'none';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
checkInitial2FAStatus();
|
|
356
|
+
|
|
357
|
+
// --- COPY RECOVERY CODES ---
|
|
358
|
+
if (copyBtn) {
|
|
359
|
+
copyBtn.addEventListener('click', function() {
|
|
360
|
+
const codes = Array.from(recoveryList.querySelectorAll('li')).map(li => li.textContent).join('\n');
|
|
361
|
+
navigator.clipboard.writeText(codes).then(() => {
|
|
362
|
+
copyMessageEl.textContent = 'Copied to clipboard!';
|
|
363
|
+
setTimeout(() => copyMessageEl.textContent = '', 2000);
|
|
364
|
+
}).catch(err => {
|
|
365
|
+
copyMessageEl.textContent = 'Failed to copy.';
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// ADD AUTHENTICATION METHOD
|
|
373
|
+
// ============================================================================
|
|
374
|
+
|
|
375
|
+
async function handleAddAuthMethodSubmit(event) {
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
const form = event.target;
|
|
378
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
379
|
+
const methodEl = form.querySelector('[data-mindfulauth-field="method"]');
|
|
380
|
+
const passwordEl = form.querySelector('[data-mindfulauth-field="new-password"]');
|
|
381
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
382
|
+
|
|
383
|
+
const selectedMethod = methodEl.querySelector('input[name="method"]:checked')?.value;
|
|
384
|
+
|
|
385
|
+
if (!selectedMethod) {
|
|
386
|
+
messageEl.textContent = 'Please select an authentication method.';
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (selectedMethod === 'Password') {
|
|
391
|
+
if (!passwordEl.value || passwordEl.value.length < 8) {
|
|
392
|
+
messageEl.textContent = 'Password must be at least 8 characters.';
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
messageEl.textContent = 'Adding authentication method...';
|
|
398
|
+
if (submitBtn) {
|
|
399
|
+
submitBtn.disabled = true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const body = {
|
|
404
|
+
method: selectedMethod
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
if (selectedMethod === 'Password') {
|
|
408
|
+
body.password = passwordEl.value;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const response = await window.apiFetch('/auth/add-authentication-method', {
|
|
412
|
+
body: JSON.stringify(body)
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const result = await response.json();
|
|
416
|
+
|
|
417
|
+
if (result.success) {
|
|
418
|
+
messageEl.textContent = result.message || `${selectedMethod} authentication has been added to your account.`;
|
|
419
|
+
messageEl.style.color = 'green';
|
|
420
|
+
|
|
421
|
+
if (passwordEl) {
|
|
422
|
+
passwordEl.value = '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
event.target.style.display = 'none';
|
|
427
|
+
}, 2000);
|
|
428
|
+
} else {
|
|
429
|
+
throw new Error(result.message || 'Failed to add authentication method.');
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
messageEl.textContent = `Error: ${error.message}`;
|
|
433
|
+
messageEl.style.color = 'red';
|
|
434
|
+
} finally {
|
|
435
|
+
if (submitBtn) {
|
|
436
|
+
submitBtn.disabled = false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function handleMethodChange(event) {
|
|
442
|
+
const passwordContainerEl = document.querySelector('[data-mindfulauth-field="password-container"]');
|
|
443
|
+
|
|
444
|
+
if (!passwordContainerEl) return;
|
|
445
|
+
|
|
446
|
+
if (event.target.value === 'Password') {
|
|
447
|
+
passwordContainerEl.removeAttribute('hidden');
|
|
448
|
+
if (passwordContainerEl.classList) {
|
|
449
|
+
passwordContainerEl.classList.remove('hidden');
|
|
450
|
+
}
|
|
451
|
+
passwordContainerEl.style.display = 'flex';
|
|
452
|
+
} else {
|
|
453
|
+
passwordContainerEl.style.display = 'none';
|
|
454
|
+
if (passwordContainerEl.classList) {
|
|
455
|
+
passwordContainerEl.classList.add('hidden');
|
|
456
|
+
}
|
|
457
|
+
passwordContainerEl.setAttribute('hidden', '');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function initAddAuthMethod() {
|
|
462
|
+
const form = document.querySelector('[data-mindfulauth-form="add-auth"]');
|
|
463
|
+
if (!form) return;
|
|
464
|
+
|
|
465
|
+
form.addEventListener('submit', handleAddAuthMethodSubmit);
|
|
466
|
+
|
|
467
|
+
// Bind radio change handlers
|
|
468
|
+
const radios = document.querySelectorAll('input[name="method"]');
|
|
469
|
+
radios.forEach(radio => {
|
|
470
|
+
radio.addEventListener('change', handleMethodChange);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Set initial state
|
|
474
|
+
const checkedRadio = document.querySelector('input[name="method"]:checked');
|
|
475
|
+
if (checkedRadio) {
|
|
476
|
+
handleMethodChange({ target: checkedRadio });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ============================================================================
|
|
481
|
+
// MAIN INITIALIZATION
|
|
482
|
+
// ============================================================================
|
|
483
|
+
|
|
484
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
485
|
+
// Initialize all security modules
|
|
486
|
+
initChangePassword();
|
|
487
|
+
init2FA();
|
|
488
|
+
initAddAuthMethod();
|
|
489
|
+
});
|
|
490
|
+
</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>
|