@xbg.solutions/bpsk-utils-firebase-auth 1.2.3
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/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/services/auth/auth.service.d.ts +89 -0
- package/lib/services/auth/auth.service.d.ts.map +1 -0
- package/lib/services/auth/auth.service.js +615 -0
- package/lib/services/auth/auth.service.js.map +1 -0
- package/lib/services/auth/email-link.d.ts +99 -0
- package/lib/services/auth/email-link.d.ts.map +1 -0
- package/lib/services/auth/email-link.js +715 -0
- package/lib/services/auth/email-link.js.map +1 -0
- package/lib/services/auth/index.d.ts +15 -0
- package/lib/services/auth/index.d.ts.map +1 -0
- package/lib/services/auth/index.js +18 -0
- package/lib/services/auth/index.js.map +1 -0
- package/lib/services/auth/phone-auth.d.ts +65 -0
- package/lib/services/auth/phone-auth.d.ts.map +1 -0
- package/lib/services/auth/phone-auth.js +150 -0
- package/lib/services/auth/phone-auth.js.map +1 -0
- package/lib/services/auth/user-creation.d.ts +17 -0
- package/lib/services/auth/user-creation.d.ts.map +1 -0
- package/lib/services/auth/user-creation.js +39 -0
- package/lib/services/auth/user-creation.js.map +1 -0
- package/lib/services/token/index.d.ts +29 -0
- package/lib/services/token/index.d.ts.map +1 -0
- package/lib/services/token/index.js +20 -0
- package/lib/services/token/index.js.map +1 -0
- package/lib/services/token/token.service.d.ts +57 -0
- package/lib/services/token/token.service.d.ts.map +1 -0
- package/lib/services/token/token.service.js +554 -0
- package/lib/services/token/token.service.js.map +1 -0
- package/lib/stores/auth.service.d.ts +6 -0
- package/lib/stores/auth.service.d.ts.map +1 -0
- package/lib/stores/auth.service.js +6 -0
- package/lib/stores/auth.service.js.map +1 -0
- package/lib/stores/auth.store.d.ts +56 -0
- package/lib/stores/auth.store.d.ts.map +1 -0
- package/lib/stores/auth.store.js +64 -0
- package/lib/stores/auth.store.js.map +1 -0
- package/lib/stores/token.store.d.ts +41 -0
- package/lib/stores/token.store.d.ts.map +1 -0
- package/lib/stores/token.store.js +36 -0
- package/lib/stores/token.store.js.map +1 -0
- package/lib/stores/user-creation.d.ts +8 -0
- package/lib/stores/user-creation.d.ts.map +1 -0
- package/lib/stores/user-creation.js +11 -0
- package/lib/stores/user-creation.js.map +1 -0
- package/lib/utils/auth-guard.d.ts +58 -0
- package/lib/utils/auth-guard.d.ts.map +1 -0
- package/lib/utils/auth-guard.js +109 -0
- package/lib/utils/auth-guard.js.map +1 -0
- package/lib/utils/signout.d.ts +82 -0
- package/lib/utils/signout.d.ts.map +1 -0
- package/lib/utils/signout.js +168 -0
- package/lib/utils/signout.js.map +1 -0
- package/lib/utils/tokens.d.ts +136 -0
- package/lib/utils/tokens.d.ts.map +1 -0
- package/lib/utils/tokens.js +479 -0
- package/lib/utils/tokens.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/services/auth/email-link.ts
|
|
3
|
+
* Email Link Authentication service
|
|
4
|
+
*
|
|
5
|
+
* Provides functionality for Firebase Email Link authentication:
|
|
6
|
+
* - Sending authentication links
|
|
7
|
+
* - Verifying links
|
|
8
|
+
* - Managing email persistence
|
|
9
|
+
* - Error handling specific to email link flow
|
|
10
|
+
*/
|
|
11
|
+
import { sendSignInLinkToEmail, isSignInWithEmailLink, signInWithEmailLink, getAuth } from 'firebase/auth';
|
|
12
|
+
import { getFirebaseAuth, getFirebaseState, processFirebaseError } from '@xbg.solutions/bpsk-core';
|
|
13
|
+
import { secureStorage } from '@xbg.solutions/bpsk-utils-secure-storage';
|
|
14
|
+
import { loggerService } from '@xbg.solutions/bpsk-core';
|
|
15
|
+
import { publish } from '@xbg.solutions/bpsk-core';
|
|
16
|
+
import { AppError, withErrorHandling } from '@xbg.solutions/bpsk-core';
|
|
17
|
+
import { AUTH_EVENTS, AUTH_CONFIG } from '@xbg.solutions/bpsk-core';
|
|
18
|
+
import { AUTH_NAMESPACE, EMAIL_FOR_SIGN_IN_KEY } from '@xbg.solutions/bpsk-utils-secure-storage';
|
|
19
|
+
// Create a context-aware logger
|
|
20
|
+
const emailLinkLogger = loggerService.withContext('EmailLinkService');
|
|
21
|
+
// Constants for verification tracking
|
|
22
|
+
const VERIFICATION_ATTEMPT_KEY = 'email_verification_attempt';
|
|
23
|
+
/**
|
|
24
|
+
* Tracks a verification attempt to prevent loops and handle reloads properly
|
|
25
|
+
*/
|
|
26
|
+
function trackVerificationAttempt(oobCode) {
|
|
27
|
+
// Get existing attempt data if any
|
|
28
|
+
const existing = secureStorage.getItem(VERIFICATION_ATTEMPT_KEY, {
|
|
29
|
+
namespace: AUTH_NAMESPACE,
|
|
30
|
+
mechanism: 'localStorage'
|
|
31
|
+
});
|
|
32
|
+
if (existing && existing.oobCode === oobCode) {
|
|
33
|
+
// Update attempts count for existing verification
|
|
34
|
+
secureStorage.setItem(VERIFICATION_ATTEMPT_KEY, {
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
oobCode,
|
|
37
|
+
attempts: existing.attempts + 1
|
|
38
|
+
}, {
|
|
39
|
+
namespace: AUTH_NAMESPACE,
|
|
40
|
+
mechanism: 'localStorage',
|
|
41
|
+
ttl: 300 // 5 minutes
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// First attempt for this verification code
|
|
46
|
+
secureStorage.setItem(VERIFICATION_ATTEMPT_KEY, {
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
oobCode,
|
|
49
|
+
attempts: 1
|
|
50
|
+
}, {
|
|
51
|
+
namespace: AUTH_NAMESPACE,
|
|
52
|
+
mechanism: 'localStorage',
|
|
53
|
+
ttl: 300 // 5 minutes
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets information about the current verification attempt
|
|
59
|
+
*/
|
|
60
|
+
function getVerificationAttempt(oobCode) {
|
|
61
|
+
if (!oobCode) {
|
|
62
|
+
return { isRepeat: false, attempts: 0 };
|
|
63
|
+
}
|
|
64
|
+
const stored = secureStorage.getItem(VERIFICATION_ATTEMPT_KEY, {
|
|
65
|
+
namespace: AUTH_NAMESPACE,
|
|
66
|
+
mechanism: 'localStorage',
|
|
67
|
+
fallbackMechanisms: ['sessionStorage'] // Added fallback for backward compatibility
|
|
68
|
+
});
|
|
69
|
+
if (stored && stored.oobCode === oobCode) {
|
|
70
|
+
return {
|
|
71
|
+
isRepeat: true,
|
|
72
|
+
attempts: stored.attempts,
|
|
73
|
+
oobCode: stored.oobCode
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { isRepeat: false, attempts: 0 };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sends an email authentication link
|
|
80
|
+
*
|
|
81
|
+
* @param options Email link configuration options
|
|
82
|
+
* @returns Promise resolving to success status
|
|
83
|
+
*/
|
|
84
|
+
// Helper function that will be reimplemented in the future
|
|
85
|
+
export async function checkUserExistsAndCreate(email) {
|
|
86
|
+
// Feature currently disabled, always return true to continue normal flow
|
|
87
|
+
emailLinkLogger.info('User creation on first sign-in is disabled');
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
export async function sendEmailLink(options) {
|
|
91
|
+
const timerId = emailLinkLogger.startTimer('sendEmailLink');
|
|
92
|
+
try {
|
|
93
|
+
emailLinkLogger.info('Sending email authentication link', { email: options.email });
|
|
94
|
+
// Auto-creation is disabled, but we'll keep the infrastructure for future reimplementation
|
|
95
|
+
// This section will be reimplemented differently in the future
|
|
96
|
+
const auth = await getFirebaseAuth();
|
|
97
|
+
// Prepare action code settings
|
|
98
|
+
const actionCodeSettings = options.actionCodeSettings || {
|
|
99
|
+
url: AUTH_CONFIG.DEFAULT_EMAIL_LINK_REDIRECT,
|
|
100
|
+
handleCodeInApp: true
|
|
101
|
+
};
|
|
102
|
+
// Send the email link
|
|
103
|
+
await sendSignInLinkToEmail(auth, options.email, actionCodeSettings);
|
|
104
|
+
// Store the email if requested
|
|
105
|
+
if (options.rememberEmail !== false) {
|
|
106
|
+
try {
|
|
107
|
+
// Use our standardized storeEmail function to ensure consistency
|
|
108
|
+
const success = storeEmail(options.email);
|
|
109
|
+
if (success) {
|
|
110
|
+
emailLinkLogger.info('Successfully stored email for authentication');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
emailLinkLogger.warn('Failed to store email using storeEmail function');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
emailLinkLogger.warn('Error storing email', err instanceof Error ? err : new Error(String(err)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Publish event
|
|
121
|
+
publish(AUTH_EVENTS.EMAIL_LINK_SENT, {
|
|
122
|
+
email: options.email,
|
|
123
|
+
context: { timestamp: Date.now() }
|
|
124
|
+
}, 'EmailLinkService');
|
|
125
|
+
emailLinkLogger.endTimer(timerId, { success: true });
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
method: 'emailLink'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
emailLinkLogger.endTimer(timerId, { success: false });
|
|
133
|
+
const processedError = processFirebaseError(error, 'Failed to send email authentication link', {
|
|
134
|
+
action: 'auth/email-link-send',
|
|
135
|
+
userMessage: 'We couldn\'t send the authentication link. Please check your email address and try again.'
|
|
136
|
+
});
|
|
137
|
+
emailLinkLogger.error('Email link sending failed', processedError, {
|
|
138
|
+
email: options.email
|
|
139
|
+
});
|
|
140
|
+
// Publish failure event
|
|
141
|
+
publish(AUTH_EVENTS.LOGIN_FAILURE, {
|
|
142
|
+
error: {
|
|
143
|
+
message: processedError.message,
|
|
144
|
+
code: processedError.firebaseCode
|
|
145
|
+
},
|
|
146
|
+
method: 'emailLink',
|
|
147
|
+
context: { email: options.email }
|
|
148
|
+
}, 'EmailLinkService');
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: processedError,
|
|
152
|
+
method: 'emailLink'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Verifies an email authentication link and signs in the user
|
|
158
|
+
*
|
|
159
|
+
* @param options Email link verification options
|
|
160
|
+
* @returns Promise resolving to authentication result
|
|
161
|
+
*/
|
|
162
|
+
export async function verifyEmailLink(options = {}) {
|
|
163
|
+
const timerId = emailLinkLogger.startTimer('verifyEmailLink');
|
|
164
|
+
try {
|
|
165
|
+
// Publish verification start event
|
|
166
|
+
publish(AUTH_EVENTS.EMAIL_LINK_VERIFICATION_START, {
|
|
167
|
+
context: { timestamp: Date.now() }
|
|
168
|
+
}, 'EmailLinkService');
|
|
169
|
+
const auth = await getFirebaseAuth();
|
|
170
|
+
// Get the email link
|
|
171
|
+
const link = options.link || window.location.href;
|
|
172
|
+
// Extract oobCode from link for verification tracking
|
|
173
|
+
let oobCode = null;
|
|
174
|
+
try {
|
|
175
|
+
const url = new URL(link);
|
|
176
|
+
oobCode = url.searchParams.get('oobCode');
|
|
177
|
+
if (oobCode) {
|
|
178
|
+
// Track this verification attempt
|
|
179
|
+
trackVerificationAttempt(oobCode);
|
|
180
|
+
// Check for potential reload loops
|
|
181
|
+
const verificationAttempt = getVerificationAttempt(oobCode);
|
|
182
|
+
if (verificationAttempt.attempts > 5) {
|
|
183
|
+
emailLinkLogger.warn('Excessive verification attempts detected', {
|
|
184
|
+
attempts: verificationAttempt.attempts,
|
|
185
|
+
oobCode
|
|
186
|
+
});
|
|
187
|
+
// Don't throw an error here, continue with verification
|
|
188
|
+
// but log the warning
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
// If URL parsing fails, just continue without tracking
|
|
194
|
+
emailLinkLogger.warn('Failed to extract oobCode for tracking', {
|
|
195
|
+
error: e instanceof Error ? e.message : String(e)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// Check if the link is valid
|
|
199
|
+
if (!isSignInWithEmailLink(auth, link)) {
|
|
200
|
+
const error = new AppError('Invalid email link', {
|
|
201
|
+
category: 'auth',
|
|
202
|
+
userMessage: 'The authentication link is invalid. Please request a new link.',
|
|
203
|
+
context: { link }
|
|
204
|
+
});
|
|
205
|
+
emailLinkLogger.error('Email link verification failed', error);
|
|
206
|
+
// Publish failure event
|
|
207
|
+
publish(AUTH_EVENTS.EMAIL_LINK_VERIFICATION_FAILURE, {
|
|
208
|
+
error: {
|
|
209
|
+
message: error.message,
|
|
210
|
+
code: undefined
|
|
211
|
+
},
|
|
212
|
+
method: 'emailLink'
|
|
213
|
+
}, 'EmailLinkService');
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error,
|
|
217
|
+
method: 'emailLink'
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Get the email address
|
|
221
|
+
let email = options.email;
|
|
222
|
+
if (!email) {
|
|
223
|
+
// Try to get email from secure storage - first from localStorage, then from sessionStorage
|
|
224
|
+
const storedEmail = secureStorage.getItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
225
|
+
namespace: AUTH_NAMESPACE,
|
|
226
|
+
mechanism: 'localStorage',
|
|
227
|
+
fallbackMechanisms: ['sessionStorage'] // Added fallback for backward compatibility
|
|
228
|
+
});
|
|
229
|
+
// IMPORTANT: We don't accept email from URL for security reasons
|
|
230
|
+
// Check for stored email
|
|
231
|
+
if (!storedEmail) {
|
|
232
|
+
const error = new AppError('Email not found', {
|
|
233
|
+
category: 'auth',
|
|
234
|
+
userMessage: 'Please provide the email address you used to request the link.',
|
|
235
|
+
context: { link }
|
|
236
|
+
});
|
|
237
|
+
emailLinkLogger.error('Email link verification failed', error);
|
|
238
|
+
// Publish failure event
|
|
239
|
+
publish(AUTH_EVENTS.EMAIL_LINK_VERIFICATION_FAILURE, {
|
|
240
|
+
error: {
|
|
241
|
+
message: error.message,
|
|
242
|
+
code: undefined
|
|
243
|
+
},
|
|
244
|
+
method: 'emailLink'
|
|
245
|
+
}, 'EmailLinkService');
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error,
|
|
249
|
+
method: 'emailLink'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
email = storedEmail;
|
|
253
|
+
}
|
|
254
|
+
emailLinkLogger.info('Verifying email link', {
|
|
255
|
+
email,
|
|
256
|
+
link,
|
|
257
|
+
isValid: isSignInWithEmailLink(auth, link)
|
|
258
|
+
});
|
|
259
|
+
// Sign in with the email link
|
|
260
|
+
console.log('Attempting to sign in with:', { email, link });
|
|
261
|
+
const credential = await signInWithEmailLink(auth, email, link);
|
|
262
|
+
const user = credential.user;
|
|
263
|
+
// Check verification attempt count before cleaning up email
|
|
264
|
+
const verificationAttempt = getVerificationAttempt(oobCode ?? undefined);
|
|
265
|
+
// IMPORTANT: Keep email until token verification is complete
|
|
266
|
+
// Don't remove the email immediately to avoid issues with session establishment
|
|
267
|
+
// We'll use a short TTL as a fallback in case the token event never triggers
|
|
268
|
+
// Keep the email temporarily to ensure token verification completes
|
|
269
|
+
// NOTE: The Firebase token will be the source of truth for authentication,
|
|
270
|
+
// not the email in storage. The token persists via Firebase's own mechanisms.
|
|
271
|
+
try {
|
|
272
|
+
// Use our standard storage mechanism for consistency
|
|
273
|
+
// This is just for temporary storage until the token is established
|
|
274
|
+
window.localStorage.setItem('emailForSignIn', email);
|
|
275
|
+
// Also store in memory as a backup
|
|
276
|
+
secureStorage.setItem(EMAIL_FOR_SIGN_IN_KEY, email, {
|
|
277
|
+
namespace: AUTH_NAMESPACE,
|
|
278
|
+
mechanism: 'memory',
|
|
279
|
+
ttl: 300 // 5 minutes as fallback if token event doesn't trigger
|
|
280
|
+
});
|
|
281
|
+
emailLinkLogger.info('Keeping email in storage until token is established', {
|
|
282
|
+
email,
|
|
283
|
+
attempts: verificationAttempt.attempts
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
emailLinkLogger.warn('Error keeping email in storage during verification', err instanceof Error ? err : new Error(String(err)));
|
|
288
|
+
// Non-fatal error, continue with verification
|
|
289
|
+
}
|
|
290
|
+
// DETERMINISTIC CLEANUP WITH MULTIPLE FALLBACKS
|
|
291
|
+
// Approach 1: Immediate cleanup attempt as soon as we have a successful login
|
|
292
|
+
// This is the primary deterministic cleanup
|
|
293
|
+
try {
|
|
294
|
+
// If we've already successfully signed in, we can clear immediately
|
|
295
|
+
const auth = getAuth();
|
|
296
|
+
if (auth && auth.currentUser) {
|
|
297
|
+
emailLinkLogger.info('User already authenticated, clearing stored email immediately');
|
|
298
|
+
clearStoredEmail();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
emailLinkLogger.warn('Error during immediate cleanup check', e instanceof Error ? e : new Error(String(e)));
|
|
303
|
+
}
|
|
304
|
+
// Approach 2: Delayed cleanup after token processing is complete
|
|
305
|
+
// This is for cases where auth state takes a moment to propagate
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
try {
|
|
308
|
+
const auth = getAuth();
|
|
309
|
+
if (auth && auth.currentUser) {
|
|
310
|
+
emailLinkLogger.info('User authenticated after delay, clearing stored email');
|
|
311
|
+
clearStoredEmail();
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
emailLinkLogger.info('No authenticated user found after delay, email will be cleared by TTL');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
emailLinkLogger.warn('Error checking auth state after verification delay', err instanceof Error ? err : new Error(String(err)));
|
|
319
|
+
}
|
|
320
|
+
}, 2000); // Wait 2 seconds to ensure token processing is complete
|
|
321
|
+
// Approach 3: Subscribe to auth state changes
|
|
322
|
+
// This catches auth state changes that might happen after our timeouts
|
|
323
|
+
try {
|
|
324
|
+
const unsubscribe = getAuth().onAuthStateChanged((user) => {
|
|
325
|
+
if (user) {
|
|
326
|
+
emailLinkLogger.info('Auth state changed to authenticated, clearing stored email');
|
|
327
|
+
clearStoredEmail();
|
|
328
|
+
// Cleanup subscription after first auth state change
|
|
329
|
+
unsubscribe();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// Ensure the subscription is cleaned up after a reasonable time
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
try {
|
|
335
|
+
unsubscribe();
|
|
336
|
+
emailLinkLogger.info('Cleaned up auth state subscription');
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
// Ignore any errors in unsubscribe
|
|
340
|
+
}
|
|
341
|
+
}, 5000); // 5 seconds should be more than enough time for auth to complete
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
emailLinkLogger.warn('Could not set up auth state change listener', e instanceof Error ? e : new Error(String(e)));
|
|
345
|
+
}
|
|
346
|
+
// Approach 4: TTL-based cleanup
|
|
347
|
+
// The memory storage already has a TTL of 300 seconds (5 minutes)
|
|
348
|
+
// For localStorage, we'll add a timestamp that our getStoredEmail function can check
|
|
349
|
+
try {
|
|
350
|
+
// Add expiration timestamp to localStorage
|
|
351
|
+
const expiresAt = Date.now() + (300 * 1000); // 5 minutes
|
|
352
|
+
window.localStorage.setItem('emailForSignIn_expiresAt', expiresAt.toString());
|
|
353
|
+
emailLinkLogger.info('Added expiration timestamp to email storage as final fallback');
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
// Not critical, the other cleanup methods should work
|
|
357
|
+
}
|
|
358
|
+
// Publish success event
|
|
359
|
+
publish(AUTH_EVENTS.EMAIL_LINK_VERIFICATION_SUCCESS, {
|
|
360
|
+
user,
|
|
361
|
+
method: 'emailLink',
|
|
362
|
+
context: { email }
|
|
363
|
+
}, 'EmailLinkService');
|
|
364
|
+
emailLinkLogger.endTimer(timerId, { success: true });
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
user,
|
|
368
|
+
credential,
|
|
369
|
+
method: 'emailLink'
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
emailLinkLogger.endTimer(timerId, { success: false });
|
|
374
|
+
const processedError = processFirebaseError(error, 'Failed to verify email link', {
|
|
375
|
+
action: 'auth/email-link-verify',
|
|
376
|
+
userMessage: 'We couldn\'t verify the authentication link. It may have expired or already been used.'
|
|
377
|
+
});
|
|
378
|
+
emailLinkLogger.error('Email link verification failed', processedError);
|
|
379
|
+
// Publish failure event
|
|
380
|
+
publish(AUTH_EVENTS.EMAIL_LINK_VERIFICATION_FAILURE, {
|
|
381
|
+
error: {
|
|
382
|
+
message: processedError.message,
|
|
383
|
+
code: processedError.firebaseCode
|
|
384
|
+
},
|
|
385
|
+
method: 'emailLink'
|
|
386
|
+
}, 'EmailLinkService');
|
|
387
|
+
return {
|
|
388
|
+
success: false,
|
|
389
|
+
error: processedError,
|
|
390
|
+
method: 'emailLink'
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Safely sends an email authentication link with error handling
|
|
396
|
+
*/
|
|
397
|
+
export const safeSendEmailLink = withErrorHandling(sendEmailLink);
|
|
398
|
+
/**
|
|
399
|
+
* Safely verifies an email authentication link with error handling
|
|
400
|
+
*/
|
|
401
|
+
export const safeVerifyEmailLink = withErrorHandling(verifyEmailLink);
|
|
402
|
+
/**
|
|
403
|
+
* Checks if the current URL is an email sign-in link
|
|
404
|
+
*
|
|
405
|
+
* @returns Promise resolving to whether the URL is an email sign-in link
|
|
406
|
+
*/
|
|
407
|
+
export async function isEmailSignInLink() {
|
|
408
|
+
try {
|
|
409
|
+
const auth = await getFirebaseAuth(); // Await the Promise
|
|
410
|
+
const currentUrl = window.location.href;
|
|
411
|
+
// Log the check for debugging
|
|
412
|
+
console.log('Checking if URL is email sign-in link:', currentUrl);
|
|
413
|
+
const isValid = isSignInWithEmailLink(auth, currentUrl);
|
|
414
|
+
console.log('Firebase reports URL is valid:', isValid);
|
|
415
|
+
return isValid;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
emailLinkLogger.error('Failed to check if URL is email sign-in link', error instanceof Error ? error : new Error(String(error)));
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Synchronous version that can be used in places where async isn't supported
|
|
424
|
+
* This uses the current auth instance if available, otherwise returns false
|
|
425
|
+
*/
|
|
426
|
+
export function isEmailSignInLinkSync() {
|
|
427
|
+
try {
|
|
428
|
+
// Get the current Firebase state without async initialization
|
|
429
|
+
const firebaseState = getFirebaseState();
|
|
430
|
+
// If Firebase isn't initialized or has an error, return false
|
|
431
|
+
if (!firebaseState.initialized || firebaseState.error || !firebaseState.auth) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
const auth = firebaseState.auth;
|
|
435
|
+
const currentUrl = window.location.href;
|
|
436
|
+
return isSignInWithEmailLink(auth, currentUrl);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
emailLinkLogger.error('Failed to check if URL is email sign-in link (sync)', error instanceof Error ? error : new Error(String(error)));
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Gets the stored email for sign-in
|
|
445
|
+
*
|
|
446
|
+
* @returns The stored email or null if not found
|
|
447
|
+
*/
|
|
448
|
+
export function getStoredEmail() {
|
|
449
|
+
try {
|
|
450
|
+
let foundEmail = null;
|
|
451
|
+
// Primary source: Firebase standard location - localStorage with key 'emailForSignIn'
|
|
452
|
+
try {
|
|
453
|
+
const email = window.localStorage.getItem('emailForSignIn');
|
|
454
|
+
if (email) {
|
|
455
|
+
// Check expiration timestamp if present
|
|
456
|
+
try {
|
|
457
|
+
const expiresAtStr = window.localStorage.getItem('emailForSignIn_expiresAt');
|
|
458
|
+
if (expiresAtStr) {
|
|
459
|
+
const expiresAt = parseInt(expiresAtStr, 10);
|
|
460
|
+
if (!isNaN(expiresAt) && Date.now() > expiresAt) {
|
|
461
|
+
// Email has expired, clean it up
|
|
462
|
+
emailLinkLogger.info('Email in localStorage has expired, clearing it');
|
|
463
|
+
window.localStorage.removeItem('emailForSignIn');
|
|
464
|
+
window.localStorage.removeItem('emailForSignIn_expiresAt');
|
|
465
|
+
// Continue to fallback mechanisms
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Email is still valid
|
|
469
|
+
emailLinkLogger.info('Retrieved valid email from localStorage', { email });
|
|
470
|
+
foundEmail = email;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// No expiration timestamp, assume valid
|
|
475
|
+
emailLinkLogger.info('Retrieved email from localStorage (no expiration)', { email });
|
|
476
|
+
foundEmail = email;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
// Error checking expiration, assume email is valid
|
|
481
|
+
emailLinkLogger.warn('Error checking email expiration', e instanceof Error ? e : new Error(String(e)));
|
|
482
|
+
foundEmail = email;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
emailLinkLogger.warn('Error accessing localStorage', e instanceof Error ? e : new Error(String(e)));
|
|
488
|
+
}
|
|
489
|
+
// Return if we found an email
|
|
490
|
+
if (foundEmail) {
|
|
491
|
+
return foundEmail;
|
|
492
|
+
}
|
|
493
|
+
// Fallback 1: Try sessionStorage
|
|
494
|
+
try {
|
|
495
|
+
const sessionEmail = window.sessionStorage.getItem('emailForSignIn');
|
|
496
|
+
if (sessionEmail) {
|
|
497
|
+
emailLinkLogger.info('Retrieved email from sessionStorage', { email: sessionEmail });
|
|
498
|
+
// Copy to localStorage for Firebase compatibility
|
|
499
|
+
try {
|
|
500
|
+
window.localStorage.setItem('emailForSignIn', sessionEmail);
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
// Non-critical error
|
|
504
|
+
}
|
|
505
|
+
return sessionEmail;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
emailLinkLogger.warn('Error accessing sessionStorage', e instanceof Error ? e : new Error(String(e)));
|
|
510
|
+
}
|
|
511
|
+
// Fallback 2: Try secureStorage with sessionStorage mechanism
|
|
512
|
+
try {
|
|
513
|
+
const secureSessionEmail = secureStorage.getItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
514
|
+
namespace: AUTH_NAMESPACE,
|
|
515
|
+
mechanism: 'sessionStorage'
|
|
516
|
+
});
|
|
517
|
+
if (secureSessionEmail) {
|
|
518
|
+
emailLinkLogger.info('Retrieved email from secure sessionStorage', { email: secureSessionEmail });
|
|
519
|
+
// Copy to localStorage for Firebase compatibility
|
|
520
|
+
try {
|
|
521
|
+
window.localStorage.setItem('emailForSignIn', secureSessionEmail);
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
// Non-critical error
|
|
525
|
+
}
|
|
526
|
+
return secureSessionEmail;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
// Non-critical error
|
|
531
|
+
}
|
|
532
|
+
// Fallback 3: Try memory storage
|
|
533
|
+
try {
|
|
534
|
+
const memoryEmail = secureStorage.getItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
535
|
+
namespace: AUTH_NAMESPACE,
|
|
536
|
+
mechanism: 'memory'
|
|
537
|
+
});
|
|
538
|
+
if (memoryEmail) {
|
|
539
|
+
emailLinkLogger.info('Retrieved email from memory storage', { email: memoryEmail });
|
|
540
|
+
// Copy to localStorage for Firebase compatibility
|
|
541
|
+
try {
|
|
542
|
+
window.localStorage.setItem('emailForSignIn', memoryEmail);
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
// Non-critical error
|
|
546
|
+
}
|
|
547
|
+
return memoryEmail;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
// Non-critical error
|
|
552
|
+
}
|
|
553
|
+
// If we get here, we couldn't find the email anywhere
|
|
554
|
+
emailLinkLogger.info('No stored email found in any storage location');
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
emailLinkLogger.error('Failed to get stored email', error instanceof Error ? error : new Error(String(error)));
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Stores an email for sign-in
|
|
564
|
+
*
|
|
565
|
+
* IMPORTANT: We use localStorage WITHOUT encryption to ensure cross-tab compatibility.
|
|
566
|
+
* This is a necessary security trade-off for email link authentication which requires
|
|
567
|
+
* the email to be available in any tab where the user opens the verification link.
|
|
568
|
+
*
|
|
569
|
+
* @param email Email to store
|
|
570
|
+
* @returns Whether storage was successful
|
|
571
|
+
*/
|
|
572
|
+
export function storeEmail(email) {
|
|
573
|
+
try {
|
|
574
|
+
let success = false;
|
|
575
|
+
let sessionStorageSuccess = false;
|
|
576
|
+
// IMPORTANT: Firebase's email link auth specifically looks for 'emailForSignIn'
|
|
577
|
+
// in localStorage (not in secure storage). This is the standard location where
|
|
578
|
+
// Firebase expects to find the email.
|
|
579
|
+
try {
|
|
580
|
+
// 1. Store in localStorage (Firebase's standard location)
|
|
581
|
+
window.localStorage.setItem('emailForSignIn', email);
|
|
582
|
+
// 2. Add expiration timestamp for automatic cleanup
|
|
583
|
+
const expiresAt = Date.now() + (60 * 60 * 1000); // 1 hour
|
|
584
|
+
window.localStorage.setItem('emailForSignIn_expiresAt', expiresAt.toString());
|
|
585
|
+
success = true;
|
|
586
|
+
// 3. Also store in sessionStorage as backup
|
|
587
|
+
try {
|
|
588
|
+
window.sessionStorage.setItem('emailForSignIn', email);
|
|
589
|
+
sessionStorageSuccess = true;
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
// Non-critical error
|
|
593
|
+
}
|
|
594
|
+
emailLinkLogger.info('Stored email directly in localStorage for Firebase compatibility');
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
emailLinkLogger.warn('Failed to store email in localStorage', { error: err instanceof Error ? err.message : String(err) });
|
|
598
|
+
}
|
|
599
|
+
// Also keep in secure storage
|
|
600
|
+
const memorySuccess = secureStorage.setItem(EMAIL_FOR_SIGN_IN_KEY, email, {
|
|
601
|
+
namespace: AUTH_NAMESPACE,
|
|
602
|
+
mechanism: 'memory',
|
|
603
|
+
ttl: AUTH_CONFIG.EMAIL_PERSISTENCE_DURATION
|
|
604
|
+
});
|
|
605
|
+
// Store in secure storage with sessionStorage mechanism too
|
|
606
|
+
try {
|
|
607
|
+
secureStorage.setItem(EMAIL_FOR_SIGN_IN_KEY, email, {
|
|
608
|
+
namespace: AUTH_NAMESPACE,
|
|
609
|
+
mechanism: 'sessionStorage',
|
|
610
|
+
ttl: AUTH_CONFIG.EMAIL_PERSISTENCE_DURATION * 2 // Double the TTL for extra safety
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
// Non-critical error
|
|
615
|
+
}
|
|
616
|
+
emailLinkLogger.info('Email stored for auth', {
|
|
617
|
+
directStorage: true,
|
|
618
|
+
localStorage: success,
|
|
619
|
+
sessionStorage: sessionStorageSuccess,
|
|
620
|
+
memory: memorySuccess
|
|
621
|
+
});
|
|
622
|
+
return success;
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
emailLinkLogger.error('Failed to store email', error instanceof Error ? error : new Error(String(error)));
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Clears the stored email for sign-in
|
|
631
|
+
*
|
|
632
|
+
* @returns Whether removal was successful
|
|
633
|
+
*/
|
|
634
|
+
export function clearStoredEmail() {
|
|
635
|
+
try {
|
|
636
|
+
let success = false;
|
|
637
|
+
let sessionSuccess = false;
|
|
638
|
+
// Clear from localStorage (Firebase's standard location)
|
|
639
|
+
try {
|
|
640
|
+
window.localStorage.removeItem('emailForSignIn');
|
|
641
|
+
window.localStorage.removeItem('emailForSignIn_expiresAt'); // Also clear expiration timestamp
|
|
642
|
+
success = true;
|
|
643
|
+
emailLinkLogger.info('Cleared email from localStorage');
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
emailLinkLogger.warn('Failed to clear email from localStorage', { error: err instanceof Error ? err.message : String(err) });
|
|
647
|
+
}
|
|
648
|
+
// Clear from sessionStorage
|
|
649
|
+
try {
|
|
650
|
+
window.sessionStorage.removeItem('emailForSignIn');
|
|
651
|
+
sessionSuccess = true;
|
|
652
|
+
emailLinkLogger.info('Cleared email from sessionStorage');
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
emailLinkLogger.warn('Failed to clear email from sessionStorage', { error: err instanceof Error ? err.message : String(err) });
|
|
656
|
+
}
|
|
657
|
+
// Clear from secure storage with various mechanisms
|
|
658
|
+
let memoryResult = false;
|
|
659
|
+
let secureStorageResult = false;
|
|
660
|
+
// Memory storage
|
|
661
|
+
try {
|
|
662
|
+
memoryResult = secureStorage.removeItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
663
|
+
namespace: AUTH_NAMESPACE,
|
|
664
|
+
mechanism: 'memory'
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
// Non-critical error
|
|
669
|
+
}
|
|
670
|
+
// Session storage
|
|
671
|
+
try {
|
|
672
|
+
secureStorageResult = secureStorage.removeItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
673
|
+
namespace: AUTH_NAMESPACE,
|
|
674
|
+
mechanism: 'sessionStorage'
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
// Non-critical error
|
|
679
|
+
}
|
|
680
|
+
// Local storage mechanism
|
|
681
|
+
try {
|
|
682
|
+
secureStorage.removeItem(EMAIL_FOR_SIGN_IN_KEY, {
|
|
683
|
+
namespace: AUTH_NAMESPACE,
|
|
684
|
+
mechanism: 'localStorage'
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
catch (e) {
|
|
688
|
+
// Non-critical error
|
|
689
|
+
}
|
|
690
|
+
emailLinkLogger.info('Cleared stored email from all storage mechanisms', {
|
|
691
|
+
localStorage: success,
|
|
692
|
+
sessionStorage: sessionSuccess,
|
|
693
|
+
memory: memoryResult,
|
|
694
|
+
secureStorage: secureStorageResult
|
|
695
|
+
});
|
|
696
|
+
return success || sessionSuccess || memoryResult || secureStorageResult;
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
emailLinkLogger.error('Failed to clear stored email', error instanceof Error ? error : new Error(String(error)));
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
export default {
|
|
704
|
+
sendEmailLink,
|
|
705
|
+
verifyEmailLink,
|
|
706
|
+
safeSendEmailLink,
|
|
707
|
+
safeVerifyEmailLink,
|
|
708
|
+
isEmailSignInLink,
|
|
709
|
+
isEmailSignInLinkSync,
|
|
710
|
+
getStoredEmail,
|
|
711
|
+
storeEmail,
|
|
712
|
+
clearStoredEmail,
|
|
713
|
+
checkUserExistsAndCreate
|
|
714
|
+
};
|
|
715
|
+
//# sourceMappingURL=email-link.js.map
|