@umituz/web-firebase 3.0.0 → 3.2.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.
@@ -1,75 +1,48 @@
1
1
  /**
2
2
  * User Repository Interface
3
- * @description Defines contract for user data operations
3
+ * @description Generic contract for user data operations
4
4
  */
5
5
 
6
- import type { User } from '../entities/user.entity'
7
6
  import type { QueryConstraint } from 'firebase/firestore'
8
7
 
9
- /**
10
- * User Repository Interface
11
- * Defines operations for user data management
12
- */
13
8
  export interface IUserRepository {
14
9
  /**
15
- * Get user by ID
16
- */
17
- getUser(userId: string): Promise<User | null>
18
-
19
- /**
20
- * Get user by email
21
- */
22
- getUserByEmail(email: string): Promise<User | null>
23
-
24
- /**
25
- * Create user
26
- */
27
- createUser(userId: string, data: Partial<User>): Promise<void>
28
-
29
- /**
30
- * Update user
10
+ * Get document by ID
31
11
  */
32
- updateUser(userId: string, data: Partial<User>): Promise<void>
12
+ getById<T>(userId: string): Promise<T | null>
33
13
 
34
14
  /**
35
- * Delete user
15
+ * Get document by field
36
16
  */
37
- deleteUser(userId: string): Promise<void>
17
+ getByField<T>(collectionPath: string, field: string, value: any): Promise<T | null>
38
18
 
39
19
  /**
40
- * Update user profile
20
+ * Create document
41
21
  */
42
- updateProfile(userId: string, updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL' | 'phoneNumber'>>): Promise<void>
22
+ create(userId: string, data: any): Promise<void>
43
23
 
44
24
  /**
45
- * Update user settings
25
+ * Update document
46
26
  */
47
- updateSettings(userId: string, settings: Partial<User['settings']>): Promise<void>
27
+ update(userId: string, data: any, options?: { merge?: boolean }): Promise<void>
48
28
 
49
29
  /**
50
- * Update user subscription
30
+ * Delete document
51
31
  */
52
- updateSubscription(
53
- userId: string,
54
- subscription: Partial<User['subscription']>
55
- ): Promise<void>
32
+ delete(userId: string): Promise<void>
56
33
 
57
34
  /**
58
- * Update last login timestamp
35
+ * Query collection with constraints
59
36
  */
60
- updateLastLogin(userId: string): Promise<void>
37
+ query<T>(collectionPath: string, constraints: QueryConstraint[]): Promise<T[]>
61
38
 
62
39
  /**
63
- * Query users with constraints
40
+ * Subscribe to document changes
64
41
  */
65
- queryUsers(constraints: QueryConstraint[]): Promise<User[]>
42
+ subscribeToDoc<T>(docPath: string, callback: (data: T | null) => void): () => void
66
43
 
67
44
  /**
68
- * Subscribe to user document changes
45
+ * Subscribe to collection changes
69
46
  */
70
- subscribeToUser(
71
- userId: string,
72
- callback: (user: User | null) => void,
73
- onError?: (error: Error) => void
74
- ): () => void
47
+ subscribeToCollection<T>(collectionPath: string, callback: (data: T[]) => void, constraints?: QueryConstraint[]): () => void
75
48
  }
@@ -1,162 +1,117 @@
1
1
  /**
2
2
  * Auth Service
3
- * @description Firebase Auth implementation of IAuthService
3
+ * @description Firebase Auth service with Google & Apple OAuth
4
+ * Facade pattern using AuthAdapter
4
5
  */
5
6
 
6
- import {
7
- signInWithEmailAndPassword,
8
- signInWithPopup,
9
- createUserWithEmailAndPassword,
10
- signOut as firebaseSignOut,
11
- sendPasswordResetEmail,
12
- sendEmailVerification,
13
- updateProfile as updateAuthProfile,
14
- updateEmail as updateAuthEmail,
15
- updatePassword as updateAuthPassword,
16
- reauthenticateWithCredential,
17
- EmailAuthProvider,
18
- UserCredential,
19
- } from 'firebase/auth'
20
- import { GoogleAuthProvider } from 'firebase/auth'
21
- import { getFirebaseAuth } from '../../../infrastructure/firebase/client'
22
- import type { IAuthService } from '../types'
7
+ import type { UserCredential } from 'firebase/auth'
23
8
  import type { AuthUser } from '../entities'
9
+ import type { IAuthService } from '../types'
10
+ import { authAdapter } from '../../../infrastructure/firebase/auth.adapter'
24
11
 
12
+ /**
13
+ * Auth Service
14
+ * Implements IAuthService interface using AuthAdapter
15
+ */
25
16
  class AuthService implements IAuthService {
26
- private get auth() {
27
- return getFirebaseAuth()
28
- }
29
-
30
- // Authentication Methods
17
+ // ==================== Authentication Methods ====================
31
18
 
32
19
  async signIn(email: string, password: string): Promise<UserCredential> {
33
- try {
34
- return await signInWithEmailAndPassword(this.auth, email, password)
35
- } catch (error) {
36
- throw this.handleAuthError(error)
37
- }
20
+ return await authAdapter.signIn(email, password)
38
21
  }
39
22
 
40
23
  async signUp(email: string, password: string, displayName: string): Promise<UserCredential> {
41
- try {
42
- const result = await createUserWithEmailAndPassword(this.auth, email, password)
43
-
44
- // Update profile
45
- await updateAuthProfile(result.user, { displayName })
46
-
47
- // Send email verification
48
- await sendEmailVerification(result.user)
49
-
50
- return result
51
- } catch (error) {
52
- throw this.handleAuthError(error)
53
- }
24
+ return await authAdapter.signUp(email, password, displayName)
54
25
  }
55
26
 
56
- async signInWithGoogle(): Promise<UserCredential> {
57
- try {
58
- const provider = new GoogleAuthProvider()
59
- provider.addScope('profile')
60
- provider.addScope('email')
27
+ /**
28
+ * Sign in with Google
29
+ */
30
+ async signInWithGoogle(useRedirect = false): Promise<UserCredential> {
31
+ return await authAdapter.signInWithGoogle(useRedirect)
32
+ }
61
33
 
62
- return await signInWithPopup(this.auth, provider)
63
- } catch (error) {
64
- throw this.handleAuthError(error)
65
- }
34
+ /**
35
+ * Sign in with Apple
36
+ */
37
+ async signInWithApple(useRedirect = false): Promise<UserCredential> {
38
+ return await authAdapter.signInWithApple(useRedirect)
66
39
  }
67
40
 
68
41
  async signOut(): Promise<void> {
69
- try {
70
- await firebaseSignOut(this.auth)
71
- } catch (error) {
72
- throw new Error('Sign out failed')
73
- }
42
+ await authAdapter.signOut()
74
43
  }
75
44
 
76
45
  async sendPasswordReset(email: string): Promise<void> {
77
- try {
78
- await sendPasswordResetEmail(this.auth, email)
79
- } catch (error) {
80
- throw this.handleAuthError(error)
81
- }
46
+ await authAdapter.sendPasswordReset(email)
82
47
  }
83
48
 
84
49
  async resendEmailVerification(): Promise<void> {
85
- try {
86
- const user = this.auth.currentUser
87
- if (!user) {
88
- throw new Error('No user logged in')
89
- }
90
- await sendEmailVerification(user)
91
- } catch (error) {
92
- throw new Error('Failed to resend verification')
93
- }
50
+ await authAdapter.resendEmailVerification()
94
51
  }
95
52
 
96
- // Profile Management
53
+ // ==================== Profile Management ====================
97
54
 
98
55
  async updateProfile(updates: { displayName?: string; photoURL?: string }): Promise<void> {
99
- try {
100
- const user = this.auth.currentUser
101
- if (!user) {
102
- throw new Error('No user logged in')
103
- }
104
-
105
- await updateAuthProfile(user, updates)
106
- } catch (error) {
107
- throw new Error('Profile update failed')
108
- }
56
+ await authAdapter.updateProfile(updates)
109
57
  }
110
58
 
111
59
  async updateEmail(newEmail: string, password: string): Promise<void> {
112
- try {
113
- const user = this.auth.currentUser
114
- if (!user || !user.email) {
115
- throw new Error('No user logged in')
116
- }
117
-
118
- const credential = EmailAuthProvider.credential(user.email, password)
119
- await reauthenticateWithCredential(user, credential)
120
- await updateAuthEmail(user, newEmail)
121
- } catch (error) {
122
- throw new Error('Email update failed')
123
- }
60
+ await authAdapter.updateEmail(newEmail, password)
124
61
  }
125
62
 
126
63
  async updatePassword(currentPassword: string, newPassword: string): Promise<void> {
127
- try {
128
- const user = this.auth.currentUser
129
- if (!user || !user.email) {
130
- throw new Error('No user logged in')
131
- }
132
-
133
- const credential = EmailAuthProvider.credential(user.email, currentPassword)
134
- await reauthenticateWithCredential(user, credential)
135
- await updateAuthPassword(user, newPassword)
136
- } catch (error) {
137
- throw new Error('Password update failed')
138
- }
64
+ await authAdapter.updatePassword(currentPassword, newPassword)
139
65
  }
140
66
 
141
67
  async deleteAccount(password: string): Promise<void> {
142
- try {
143
- const user = this.auth.currentUser
144
- if (!user || !user.email) {
145
- throw new Error('No user logged in')
146
- }
147
-
148
- const credential = EmailAuthProvider.credential(user.email, password)
149
- await reauthenticateWithCredential(user, credential)
150
- await user.delete()
151
- } catch (error) {
152
- throw new Error('Account deletion failed')
153
- }
68
+ await authAdapter.deleteAccount(password)
69
+ }
70
+
71
+ // ==================== Provider Linking ====================
72
+
73
+ /**
74
+ * Link Google to current user
75
+ */
76
+ async linkGoogle(): Promise<UserCredential> {
77
+ return await authAdapter.linkGoogle()
78
+ }
79
+
80
+ /**
81
+ * Link Apple to current user
82
+ */
83
+ async linkApple(): Promise<UserCredential> {
84
+ return await authAdapter.linkApple()
154
85
  }
155
86
 
156
- // State Management
87
+ /**
88
+ * Unlink a provider from current user
89
+ */
90
+ async unlinkProvider(providerId: string): Promise<void> {
91
+ await authAdapter.unlinkProvider(providerId)
92
+ }
93
+
94
+ // ==================== Token Management ====================
95
+
96
+ /**
97
+ * Get ID Token
98
+ */
99
+ async getIdToken(forceRefresh = false): Promise<string> {
100
+ return await authAdapter.getIdToken(forceRefresh)
101
+ }
102
+
103
+ /**
104
+ * Refresh ID Token
105
+ */
106
+ async refreshToken(): Promise<void> {
107
+ await authAdapter.refreshToken()
108
+ }
109
+
110
+ // ==================== State Management ====================
157
111
 
158
112
  getCurrentUser(): AuthUser | null {
159
- const user = this.auth.currentUser
113
+ const user = authAdapter.getCurrentUser()
114
+
160
115
  if (!user) return null
161
116
 
162
117
  return {
@@ -180,7 +135,7 @@ class AuthService implements IAuthService {
180
135
  callback: (user: AuthUser | null) => void,
181
136
  onError?: (error: Error) => void
182
137
  ): () => void {
183
- return this.auth.onAuthStateChanged(
138
+ return authAdapter.onAuthStateChanged(
184
139
  (user) => {
185
140
  callback(
186
141
  user
@@ -202,43 +157,9 @@ class AuthService implements IAuthService {
202
157
  : null
203
158
  )
204
159
  },
205
- (error) => {
206
- onError?.(error as Error)
207
- }
160
+ onError
208
161
  )
209
162
  }
210
-
211
- /**
212
- * Handle Firebase Auth errors
213
- */
214
- private handleAuthError(error: unknown): Error {
215
- if (error instanceof Error && 'code' in error) {
216
- const code = (error as { code: string }).code
217
-
218
- switch (code) {
219
- case 'auth/user-not-found':
220
- case 'auth/wrong-password':
221
- case 'auth/invalid-credential':
222
- return new Error('Invalid credentials')
223
- case 'auth/email-already-in-use':
224
- return new Error('Email already in use')
225
- case 'auth/weak-password':
226
- return new Error('Password is too weak')
227
- case 'auth/invalid-email':
228
- return new Error('Invalid email')
229
- case 'auth/user-disabled':
230
- return new Error('Account disabled')
231
- case 'auth/too-many-requests':
232
- return new Error('Too many requests')
233
- case 'auth/popup-closed-by-user':
234
- return new Error('Sign in cancelled')
235
- default:
236
- return new Error(`Auth error: ${code}`)
237
- }
238
- }
239
-
240
- return new Error('Unknown auth error')
241
- }
242
163
  }
243
164
 
244
165
  // Export class and singleton instance
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Auth Adapter
3
- * @description Firebase Auth implementation of IAuthRepository
4
- * Migrated from: /Users/umituz/Desktop/github/umituz/apps/web/app-growth-factory/src/domains/firebase/services/auth.ts
3
+ * @description Firebase Auth implementation with Google & Apple OAuth support
5
4
  */
6
5
 
7
6
  import {
@@ -18,21 +17,40 @@ import {
18
17
  EmailAuthProvider,
19
18
  User as FirebaseUser,
20
19
  UserCredential,
20
+ GoogleAuthProvider,
21
+ OAuthProvider,
22
+ signInWithRedirect,
23
+ getRedirectResult,
24
+ linkWithPopup,
25
+ unlink as firebaseUnlink,
21
26
  } from 'firebase/auth'
22
- import { GoogleAuthProvider } from 'firebase/auth'
23
27
  import { getFirebaseAuth } from './client'
24
28
  import type { IAuthRepository } from '../../domain/interfaces/auth.repository.interface'
25
29
  import type { User } from '../../domain/entities/user.entity'
26
30
  import { createAuthError, AuthErrorCode } from '../../domain/errors/auth.errors'
31
+ import { getAuthConfig } from '../../domain/config/auth.config'
27
32
 
33
+ /**
34
+ * Auth Adapter
35
+ * Implements IAuthRepository with Google & Apple OAuth support
36
+ */
28
37
  export class AuthAdapter implements IAuthRepository {
29
38
  private get auth() {
30
39
  return getFirebaseAuth()
31
40
  }
32
41
 
33
- // Authentication Methods
42
+ private config = getAuthConfig()
43
+
44
+ // ==================== Authentication Methods ====================
34
45
 
35
46
  async signIn(email: string, password: string): Promise<UserCredential> {
47
+ if (!this.config.isEmailPasswordEnabled()) {
48
+ throw createAuthError(
49
+ AuthErrorCode.UNKNOWN,
50
+ 'Email/password authentication is disabled'
51
+ )
52
+ }
53
+
36
54
  try {
37
55
  return await signInWithEmailAndPassword(this.auth, email, password)
38
56
  } catch (error) {
@@ -41,14 +59,23 @@ export class AuthAdapter implements IAuthRepository {
41
59
  }
42
60
 
43
61
  async signUp(email: string, password: string, displayName: string): Promise<UserCredential> {
62
+ if (!this.config.isEmailPasswordEnabled()) {
63
+ throw createAuthError(
64
+ AuthErrorCode.UNKNOWN,
65
+ 'Email/password authentication is disabled'
66
+ )
67
+ }
68
+
44
69
  try {
45
70
  const result = await createUserWithEmailAndPassword(this.auth, email, password)
46
71
 
47
72
  // Update profile
48
73
  await updateAuthProfile(result.user, { displayName })
49
74
 
50
- // Send email verification
51
- await sendEmailVerification(result.user)
75
+ // Send email verification if required
76
+ if (this.config.getConfig().requireEmailVerification) {
77
+ await sendEmailVerification(result.user)
78
+ }
52
79
 
53
80
  return result
54
81
  } catch (error) {
@@ -56,13 +83,72 @@ export class AuthAdapter implements IAuthRepository {
56
83
  }
57
84
  }
58
85
 
59
- async signInWithGoogle(): Promise<UserCredential> {
86
+ /**
87
+ * Sign in with Google
88
+ * @param useRedirect - Whether to use redirect flow instead of popup (default: false)
89
+ */
90
+ async signInWithGoogle(useRedirect = false): Promise<UserCredential> {
91
+ if (!this.config.isGoogleEnabled()) {
92
+ throw createAuthError(
93
+ AuthErrorCode.UNKNOWN,
94
+ 'Google authentication is disabled'
95
+ )
96
+ }
97
+
60
98
  try {
61
99
  const provider = new GoogleAuthProvider()
62
- provider.addScope('profile')
63
- provider.addScope('email')
100
+ const config = this.config.getConfig()
101
+
102
+ // Add scopes
103
+ if (config.googleScopes) {
104
+ config.googleScopes.forEach((scope) => provider.addScope(scope))
105
+ }
106
+
107
+ // Add custom parameters
108
+ if (config.googleCustomParameters) {
109
+ provider.setCustomParameters(config.googleCustomParameters)
110
+ }
111
+
112
+ if (useRedirect) {
113
+ await signInWithRedirect(this.auth, provider)
114
+ const result = await getRedirectResult(this.auth)
115
+ if (!result) {
116
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'No redirect result found')
117
+ }
118
+ return result
119
+ } else {
120
+ return await signInWithPopup(this.auth, provider)
121
+ }
122
+ } catch (error) {
123
+ throw this.handleAuthError(error)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Sign in with Apple
129
+ * @param useRedirect - Whether to use redirect flow instead of popup (default: false)
130
+ */
131
+ async signInWithApple(useRedirect = false): Promise<UserCredential> {
132
+ if (!this.config.isAppleEnabled()) {
133
+ throw createAuthError(
134
+ AuthErrorCode.UNKNOWN,
135
+ 'Apple authentication is disabled'
136
+ )
137
+ }
138
+
139
+ try {
140
+ const provider = new OAuthProvider('apple.com')
64
141
 
65
- return await signInWithPopup(this.auth, provider)
142
+ if (useRedirect) {
143
+ await signInWithRedirect(this.auth, provider)
144
+ const result = await getRedirectResult(this.auth)
145
+ if (!result) {
146
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'No redirect result found')
147
+ }
148
+ return result
149
+ } else {
150
+ return await signInWithPopup(this.auth, provider)
151
+ }
66
152
  } catch (error) {
67
153
  throw this.handleAuthError(error)
68
154
  }
@@ -96,7 +182,7 @@ export class AuthAdapter implements IAuthRepository {
96
182
  }
97
183
  }
98
184
 
99
- // Profile Management
185
+ // ==================== Profile Management ====================
100
186
 
101
187
  async updateProfile(
102
188
  updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL'>>
@@ -158,7 +244,77 @@ export class AuthAdapter implements IAuthRepository {
158
244
  }
159
245
  }
160
246
 
161
- // State Management
247
+ // ==================== Provider Linking ====================
248
+
249
+ /**
250
+ * Link Google to current user
251
+ */
252
+ async linkGoogle(): Promise<UserCredential> {
253
+ if (!this.config.isGoogleEnabled()) {
254
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'Google authentication is disabled')
255
+ }
256
+
257
+ try {
258
+ const user = this.auth.currentUser
259
+ if (!user) {
260
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
261
+ }
262
+
263
+ const provider = new GoogleAuthProvider()
264
+ const config = this.config.getConfig()
265
+
266
+ if (config.googleScopes) {
267
+ config.googleScopes.forEach((scope) => provider.addScope(scope))
268
+ }
269
+
270
+ if (config.googleCustomParameters) {
271
+ provider.setCustomParameters(config.googleCustomParameters)
272
+ }
273
+
274
+ return await linkWithPopup(user, provider)
275
+ } catch (error) {
276
+ throw this.handleAuthError(error)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Link Apple to current user
282
+ */
283
+ async linkApple(): Promise<UserCredential> {
284
+ if (!this.config.isAppleEnabled()) {
285
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'Apple authentication is disabled')
286
+ }
287
+
288
+ try {
289
+ const user = this.auth.currentUser
290
+ if (!user) {
291
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
292
+ }
293
+
294
+ const provider = new OAuthProvider('apple.com')
295
+ return await linkWithPopup(user, provider)
296
+ } catch (error) {
297
+ throw this.handleAuthError(error)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Unlink a provider from current user
303
+ */
304
+ async unlinkProvider(providerId: string): Promise<FirebaseUser> {
305
+ try {
306
+ const user = this.auth.currentUser
307
+ if (!user) {
308
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
309
+ }
310
+
311
+ return await firebaseUnlink(user, providerId)
312
+ } catch (error) {
313
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'Failed to unlink provider', error)
314
+ }
315
+ }
316
+
317
+ // ==================== State Management ====================
162
318
 
163
319
  getCurrentUser(): FirebaseUser | null {
164
320
  return this.auth.currentUser
@@ -168,8 +324,37 @@ export class AuthAdapter implements IAuthRepository {
168
324
  return this.auth.onAuthStateChanged(callback)
169
325
  }
170
326
 
171
- // Note: User document operations should be handled by UserAdapter
172
- // These methods are part of IAuthRepository interface but should be implemented separately
327
+ // ==================== Token Management ====================
328
+
329
+ async getIdToken(forceRefresh = false): Promise<string> {
330
+ try {
331
+ const user = this.auth.currentUser
332
+ if (!user) {
333
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
334
+ }
335
+
336
+ return await user.getIdToken(forceRefresh)
337
+ } catch (error) {
338
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'Failed to get ID token', error)
339
+ }
340
+ }
341
+
342
+ async refreshToken(): Promise<void> {
343
+ try {
344
+ const user = this.auth.currentUser
345
+ if (!user) {
346
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
347
+ }
348
+
349
+ await user.getIdToken(true)
350
+ } catch (error) {
351
+ throw createAuthError(AuthErrorCode.UNKNOWN, 'Failed to refresh token', error)
352
+ }
353
+ }
354
+
355
+ // ==================== Note ====================
356
+ // User document operations should be handled by UserAdapter
357
+
173
358
  async createUserDocument(
174
359
  _userId: string,
175
360
  _data: Partial<Omit<User, 'profile'>> & { email: string; displayName: string }
@@ -181,6 +366,8 @@ export class AuthAdapter implements IAuthRepository {
181
366
  throw new Error('updateLastLogin should be handled by UserAdapter')
182
367
  }
183
368
 
369
+ // ==================== Error Handling ====================
370
+
184
371
  /**
185
372
  * Handle Firebase Auth errors
186
373
  */
@@ -218,3 +405,6 @@ export class AuthAdapter implements IAuthRepository {
218
405
  return createAuthError(AuthErrorCode.UNKNOWN, 'Unknown auth error', error)
219
406
  }
220
407
  }
408
+
409
+ // Export singleton instance
410
+ export const authAdapter = new AuthAdapter()