@umituz/web-firebase 1.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +555 -0
- package/dist/application/index.d.mts +273 -0
- package/dist/application/index.d.ts +273 -0
- package/dist/application/index.js +490 -0
- package/dist/application/index.mjs +19 -0
- package/dist/chunk-34DL2QWQ.mjs +87 -0
- package/dist/chunk-4FP2ELQ5.mjs +96 -0
- package/dist/chunk-7TX3OU3O.mjs +721 -0
- package/dist/chunk-I6WGBPFB.mjs +439 -0
- package/dist/chunk-RZ4QR6TB.mjs +96 -0
- package/dist/chunk-U2XI4MGO.mjs +397 -0
- package/dist/domain/index.d.mts +325 -0
- package/dist/domain/index.d.ts +325 -0
- package/dist/domain/index.js +662 -0
- package/dist/domain/index.mjs +36 -0
- package/dist/file.repository.interface-v5vHgVsZ.d.mts +241 -0
- package/dist/file.repository.interface-v5vHgVsZ.d.ts +241 -0
- package/dist/firebase.entity-xvfEPjXZ.d.mts +15 -0
- package/dist/firebase.entity-xvfEPjXZ.d.ts +15 -0
- package/dist/index.d.mts +14 -96
- package/dist/index.d.ts +14 -96
- package/dist/index.js +1717 -78
- package/dist/index.mjs +88 -175
- package/dist/infrastructure/index.d.mts +170 -0
- package/dist/infrastructure/index.d.ts +170 -0
- package/dist/infrastructure/index.js +856 -0
- package/dist/infrastructure/index.mjs +46 -0
- package/dist/presentation/index.d.mts +25 -0
- package/dist/presentation/index.d.ts +25 -0
- package/dist/presentation/index.js +105 -0
- package/dist/presentation/index.mjs +6 -0
- package/dist/user.repository.interface-DS74TsJ5.d.mts +298 -0
- package/dist/user.repository.interface-DS74TsJ5.d.ts +298 -0
- package/package.json +37 -11
- package/src/application/dto/auth.dto.ts +69 -0
- package/src/application/dto/index.ts +7 -0
- package/src/application/dto/user.dto.ts +64 -0
- package/src/application/index.ts +7 -0
- package/src/application/use-cases/auth/reset-password.use-case.ts +66 -0
- package/src/application/use-cases/auth/sign-in-with-google.use-case.ts +86 -0
- package/src/application/use-cases/auth/sign-in.use-case.ts +77 -0
- package/src/application/use-cases/auth/sign-out.use-case.ts +22 -0
- package/src/application/use-cases/auth/sign-up.use-case.ts +99 -0
- package/src/application/use-cases/index.ts +12 -0
- package/src/application/use-cases/user/delete-account.use-case.ts +77 -0
- package/src/application/use-cases/user/update-profile.use-case.ts +98 -0
- package/src/domain/entities/file.entity.ts +151 -0
- package/src/domain/entities/timestamp.entity.ts +116 -0
- package/src/domain/entities/user.entity.ts +193 -0
- package/src/domain/errors/auth.errors.ts +115 -0
- package/src/domain/errors/repository.errors.ts +121 -0
- package/src/domain/index.ts +25 -2
- package/src/domain/interfaces/auth.repository.interface.ts +83 -0
- package/src/domain/interfaces/file.repository.interface.ts +143 -0
- package/src/domain/interfaces/user.repository.interface.ts +75 -0
- package/src/domain/value-objects/email.vo.ts +105 -0
- package/src/domain/value-objects/file-path.vo.ts +184 -0
- package/src/domain/value-objects/user-id.vo.ts +87 -0
- package/src/index.ts +19 -4
- package/src/infrastructure/firebase/auth.adapter.ts +220 -0
- package/src/infrastructure/firebase/client.ts +141 -0
- package/src/infrastructure/firebase/firestore.adapter.ts +190 -0
- package/src/infrastructure/firebase/storage.adapter.ts +323 -0
- package/src/infrastructure/index.ts +10 -5
- package/src/infrastructure/utils/storage.util.ts +3 -3
- package/src/presentation/hooks/useAuth.ts +108 -0
- package/src/presentation/hooks/useFirestore.ts +125 -0
- package/src/presentation/hooks/useStorage.ts +141 -0
- package/src/presentation/providers/FirebaseProvider.tsx +95 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Repository Interface
|
|
3
|
+
* @description Defines contract for user data operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { User } from '../entities/user.entity'
|
|
7
|
+
import type { QueryConstraint } from 'firebase/firestore'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* User Repository Interface
|
|
11
|
+
* Defines operations for user data management
|
|
12
|
+
*/
|
|
13
|
+
export interface IUserRepository {
|
|
14
|
+
/**
|
|
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
|
|
31
|
+
*/
|
|
32
|
+
updateUser(userId: string, data: Partial<User>): Promise<void>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Delete user
|
|
36
|
+
*/
|
|
37
|
+
deleteUser(userId: string): Promise<void>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update user profile
|
|
41
|
+
*/
|
|
42
|
+
updateProfile(userId: string, updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL' | 'phoneNumber'>>): Promise<void>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update user settings
|
|
46
|
+
*/
|
|
47
|
+
updateSettings(userId: string, settings: Partial<User['settings']>): Promise<void>
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update user subscription
|
|
51
|
+
*/
|
|
52
|
+
updateSubscription(
|
|
53
|
+
userId: string,
|
|
54
|
+
subscription: Partial<User['subscription']>
|
|
55
|
+
): Promise<void>
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update last login timestamp
|
|
59
|
+
*/
|
|
60
|
+
updateLastLogin(userId: string): Promise<void>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Query users with constraints
|
|
64
|
+
*/
|
|
65
|
+
queryUsers(constraints: QueryConstraint[]): Promise<User[]>
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe to user document changes
|
|
69
|
+
*/
|
|
70
|
+
subscribeToUser(
|
|
71
|
+
userId: string,
|
|
72
|
+
callback: (user: User | null) => void,
|
|
73
|
+
onError?: (error: Error) => void
|
|
74
|
+
): () => void
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Value Object
|
|
3
|
+
* @description Immutable email address value object with validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Email {
|
|
7
|
+
private readonly value: string
|
|
8
|
+
private readonly validated: boolean
|
|
9
|
+
|
|
10
|
+
constructor(email: string) {
|
|
11
|
+
this.value = email.trim().toLowerCase()
|
|
12
|
+
this.validated = this.isValid()
|
|
13
|
+
if (!this.validated) {
|
|
14
|
+
throw new Error(`Invalid email address: ${email}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate email format
|
|
20
|
+
*/
|
|
21
|
+
private isValid(): boolean {
|
|
22
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
23
|
+
return emailRegex.test(this.value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get email value
|
|
28
|
+
*/
|
|
29
|
+
getValue(): string {
|
|
30
|
+
return this.value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get local part (before @)
|
|
35
|
+
*/
|
|
36
|
+
getLocalPart(): string {
|
|
37
|
+
return this.value.split('@')[0]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get domain part (after @)
|
|
42
|
+
*/
|
|
43
|
+
getDomain(): string {
|
|
44
|
+
return this.value.split('@')[1] || ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if email is from a specific domain
|
|
49
|
+
*/
|
|
50
|
+
isFromDomain(domain: string): boolean {
|
|
51
|
+
return this.getDomain() === domain.toLowerCase()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if email is from a corporate domain (not gmail, yahoo, etc.)
|
|
56
|
+
*/
|
|
57
|
+
isCorporate(): boolean {
|
|
58
|
+
const freeDomains = [
|
|
59
|
+
'gmail.com',
|
|
60
|
+
'yahoo.com',
|
|
61
|
+
'hotmail.com',
|
|
62
|
+
'outlook.com',
|
|
63
|
+
'aol.com',
|
|
64
|
+
'icloud.com',
|
|
65
|
+
'protonmail.com',
|
|
66
|
+
]
|
|
67
|
+
return !freeDomains.includes(this.getDomain())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mask email for display (e.g., u***@gmail.com)
|
|
72
|
+
*/
|
|
73
|
+
mask(): string {
|
|
74
|
+
const [local, domain] = this.value.split('@')
|
|
75
|
+
if (local.length <= 2) {
|
|
76
|
+
return `${local[0]}***@${domain}`
|
|
77
|
+
}
|
|
78
|
+
return `${local[0]}${'*'.repeat(local.length - 2)}${local[local.length - 1]}@${domain}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert to string
|
|
83
|
+
*/
|
|
84
|
+
toString(): string {
|
|
85
|
+
return this.value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check equality with another email
|
|
90
|
+
*/
|
|
91
|
+
equals(other: Email): boolean {
|
|
92
|
+
return this.value === other.value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create Email from string (returns null if invalid)
|
|
97
|
+
*/
|
|
98
|
+
static create(email: string): Email | null {
|
|
99
|
+
try {
|
|
100
|
+
return new Email(email)
|
|
101
|
+
} catch {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FilePath Value Object
|
|
3
|
+
* @description Immutable file path value object with validation for Firebase Storage paths
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRepositoryError, RepositoryErrorCode } from '../errors/repository.errors'
|
|
7
|
+
|
|
8
|
+
export class FilePath {
|
|
9
|
+
private readonly value: string
|
|
10
|
+
private readonly parts: string[]
|
|
11
|
+
|
|
12
|
+
constructor(path: string) {
|
|
13
|
+
this.value = path.trim()
|
|
14
|
+
this.parts = this.value.split('/').filter(p => p.length > 0)
|
|
15
|
+
this.validate()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate file path
|
|
20
|
+
*/
|
|
21
|
+
private validate(): void {
|
|
22
|
+
if (!this.value) {
|
|
23
|
+
throw createRepositoryError(RepositoryErrorCode.INVALID_DATA, 'File path cannot be empty')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Path should not start or end with /
|
|
27
|
+
if (this.value.startsWith('/') || this.value.endsWith('/')) {
|
|
28
|
+
throw createRepositoryError(
|
|
29
|
+
RepositoryErrorCode.INVALID_DATA,
|
|
30
|
+
'File path should not start or end with /'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Path should not contain //
|
|
35
|
+
if (this.value.includes('//')) {
|
|
36
|
+
throw createRepositoryError(RepositoryErrorCode.INVALID_DATA, 'File path should not contain //')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Path segments should not be empty
|
|
40
|
+
if (this.parts.length === 0) {
|
|
41
|
+
throw createRepositoryError(RepositoryErrorCode.INVALID_DATA, 'File path must have at least one segment')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate each segment
|
|
45
|
+
for (const part of this.parts) {
|
|
46
|
+
// Segments should not contain invalid characters
|
|
47
|
+
if (/[<>:"|?*]/.test(part)) {
|
|
48
|
+
throw createRepositoryError(
|
|
49
|
+
RepositoryErrorCode.INVALID_DATA,
|
|
50
|
+
`Invalid characters in path segment: ${part}`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Segments should not be . or ..
|
|
55
|
+
if (part === '.' || part === '..') {
|
|
56
|
+
throw createRepositoryError(
|
|
57
|
+
RepositoryErrorCode.INVALID_DATA,
|
|
58
|
+
`Invalid path segment: ${part}`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get file path value
|
|
66
|
+
*/
|
|
67
|
+
getValue(): string {
|
|
68
|
+
return this.value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get path parts
|
|
73
|
+
*/
|
|
74
|
+
getParts(): string[] {
|
|
75
|
+
return [...this.parts]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get filename (last part of path)
|
|
80
|
+
*/
|
|
81
|
+
getFileName(): string {
|
|
82
|
+
return this.parts[this.parts.length - 1] || ''
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get directory path (all parts except last)
|
|
87
|
+
*/
|
|
88
|
+
getDirectory(): string {
|
|
89
|
+
return this.parts.slice(0, -1).join('/')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get file extension
|
|
94
|
+
*/
|
|
95
|
+
getExtension(): string {
|
|
96
|
+
const fileName = this.getFileName()
|
|
97
|
+
const lastDot = fileName.lastIndexOf('.')
|
|
98
|
+
return lastDot > 0 ? fileName.substring(lastDot + 1) : ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get file name without extension
|
|
103
|
+
*/
|
|
104
|
+
getFileNameWithoutExtension(): string {
|
|
105
|
+
const fileName = this.getFileName()
|
|
106
|
+
const lastDot = fileName.lastIndexOf('.')
|
|
107
|
+
return lastDot > 0 ? fileName.substring(0, lastDot) : fileName
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if path is in a specific directory
|
|
112
|
+
*/
|
|
113
|
+
isInDirectory(directory: string): boolean {
|
|
114
|
+
return this.value.startsWith(directory + '/')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if file has a specific extension
|
|
119
|
+
*/
|
|
120
|
+
hasExtension(extension: string): boolean {
|
|
121
|
+
return this.getExtension().toLowerCase() === extension.toLowerCase()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new path by appending segments
|
|
126
|
+
*/
|
|
127
|
+
append(...segments: string[]): FilePath {
|
|
128
|
+
return new FilePath([...this.parts, ...segments].join('/'))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a new path in a parent directory
|
|
133
|
+
*/
|
|
134
|
+
withParent(parent: string): FilePath {
|
|
135
|
+
return new FilePath([parent, ...this.parts].join('/'))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a new path with a different filename
|
|
140
|
+
*/
|
|
141
|
+
withFileName(fileName: string): FilePath {
|
|
142
|
+
const dir = this.getDirectory()
|
|
143
|
+
return dir ? new FilePath(`${dir}/${fileName}`) : new FilePath(fileName)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert to string
|
|
148
|
+
*/
|
|
149
|
+
toString(): string {
|
|
150
|
+
return this.value
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check equality with another file path
|
|
155
|
+
*/
|
|
156
|
+
equals(other: FilePath): boolean {
|
|
157
|
+
return this.value === other.value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create user path (users/{userId}/...)
|
|
162
|
+
*/
|
|
163
|
+
static userPath(userId: string, ...segments: string[]): FilePath {
|
|
164
|
+
return new FilePath(['users', userId, ...segments].join('/'))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create public path (public/...)
|
|
169
|
+
*/
|
|
170
|
+
static publicPath(...segments: string[]): FilePath {
|
|
171
|
+
return new FilePath(['public', ...segments].join('/'))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create FilePath from string (returns null if invalid)
|
|
176
|
+
*/
|
|
177
|
+
static create(path: string): FilePath | null {
|
|
178
|
+
try {
|
|
179
|
+
return new FilePath(path)
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserId Value Object
|
|
3
|
+
* @description Immutable user ID value object with validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRepositoryError, RepositoryErrorCode } from '../errors/repository.errors'
|
|
7
|
+
|
|
8
|
+
export class UserId {
|
|
9
|
+
private readonly value: string
|
|
10
|
+
|
|
11
|
+
constructor(id: string) {
|
|
12
|
+
this.value = id
|
|
13
|
+
this.validate()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate user ID
|
|
18
|
+
*/
|
|
19
|
+
private validate(): void {
|
|
20
|
+
if (!this.value) {
|
|
21
|
+
throw createRepositoryError(RepositoryErrorCode.INVALID_DATA, 'User ID cannot be empty')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Firebase Auth UIDs are typically at least 20 characters
|
|
25
|
+
if (this.value.length < 20) {
|
|
26
|
+
throw createRepositoryError(
|
|
27
|
+
RepositoryErrorCode.INVALID_DATA,
|
|
28
|
+
'User ID is too short (must be at least 20 characters)'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Firebase UIDs contain only alphanumeric characters and certain special characters
|
|
33
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/
|
|
34
|
+
if (!validPattern.test(this.value)) {
|
|
35
|
+
throw createRepositoryError(
|
|
36
|
+
RepositoryErrorCode.INVALID_DATA,
|
|
37
|
+
'User ID contains invalid characters'
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get user ID value
|
|
44
|
+
*/
|
|
45
|
+
getValue(): string {
|
|
46
|
+
return this.value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if this is a temporary ID (not yet persisted)
|
|
51
|
+
*/
|
|
52
|
+
isTemporary(): boolean {
|
|
53
|
+
return this.value.startsWith('temp_')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert to string
|
|
58
|
+
*/
|
|
59
|
+
toString(): string {
|
|
60
|
+
return this.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check equality with another user ID
|
|
65
|
+
*/
|
|
66
|
+
equals(other: UserId): boolean {
|
|
67
|
+
return this.value === other.value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a temporary user ID
|
|
72
|
+
*/
|
|
73
|
+
static generateTemp(): UserId {
|
|
74
|
+
return new UserId(`temp_${Date.now()}_${Math.random().toString(36).substring(7)}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create UserId from string (returns null if invalid)
|
|
79
|
+
*/
|
|
80
|
+
static create(id: string): UserId | null {
|
|
81
|
+
try {
|
|
82
|
+
return new UserId(id)
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/web-firebase
|
|
3
|
-
*
|
|
3
|
+
* Comprehensive Firebase integration with DDD architecture
|
|
4
|
+
*
|
|
5
|
+
* ONEMLI: App'ler bu root barrel'i kullanMAMALI.
|
|
6
|
+
* Subpath import kullanin:
|
|
7
|
+
* - "@umituz/web-firebase/domain" - Domain entities, value objects, interfaces
|
|
8
|
+
* - "@umituz/web-firebase/application" - Use cases and DTOs
|
|
9
|
+
* - "@umituz/web-firebase/infrastructure" - Firebase adapters
|
|
10
|
+
* - "@umituz/web-firebase/presentation" - React hooks and providers
|
|
4
11
|
*/
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
export * from './
|
|
8
|
-
|
|
13
|
+
// Domain entities, value objects, interfaces, errors
|
|
14
|
+
export * from './domain'
|
|
15
|
+
|
|
16
|
+
// Application use cases and DTOs
|
|
17
|
+
export * from './application'
|
|
18
|
+
|
|
19
|
+
// Infrastructure Firebase adapters
|
|
20
|
+
export * from './infrastructure'
|
|
21
|
+
|
|
22
|
+
// Presentation hooks and providers
|
|
23
|
+
export * from './presentation'
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
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
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
signInWithEmailAndPassword,
|
|
9
|
+
signInWithPopup,
|
|
10
|
+
createUserWithEmailAndPassword,
|
|
11
|
+
signOut as firebaseSignOut,
|
|
12
|
+
sendPasswordResetEmail,
|
|
13
|
+
sendEmailVerification,
|
|
14
|
+
updateProfile as updateAuthProfile,
|
|
15
|
+
updateEmail as updateAuthEmail,
|
|
16
|
+
updatePassword as updateAuthPassword,
|
|
17
|
+
reauthenticateWithCredential,
|
|
18
|
+
EmailAuthProvider,
|
|
19
|
+
User as FirebaseUser,
|
|
20
|
+
UserCredential,
|
|
21
|
+
} from 'firebase/auth'
|
|
22
|
+
import { GoogleAuthProvider } from 'firebase/auth'
|
|
23
|
+
import { getFirebaseAuth } from './client'
|
|
24
|
+
import type { IAuthRepository } from '../../domain/interfaces/auth.repository.interface'
|
|
25
|
+
import type { User } from '../../domain/entities/user.entity'
|
|
26
|
+
import { createAuthError, AuthErrorCode } from '../../domain/errors/auth.errors'
|
|
27
|
+
|
|
28
|
+
export class AuthAdapter implements IAuthRepository {
|
|
29
|
+
private get auth() {
|
|
30
|
+
return getFirebaseAuth()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Authentication Methods
|
|
34
|
+
|
|
35
|
+
async signIn(email: string, password: string): Promise<UserCredential> {
|
|
36
|
+
try {
|
|
37
|
+
return await signInWithEmailAndPassword(this.auth, email, password)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw this.handleAuthError(error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async signUp(email: string, password: string, displayName: string): Promise<UserCredential> {
|
|
44
|
+
try {
|
|
45
|
+
const result = await createUserWithEmailAndPassword(this.auth, email, password)
|
|
46
|
+
|
|
47
|
+
// Update profile
|
|
48
|
+
await updateAuthProfile(result.user, { displayName })
|
|
49
|
+
|
|
50
|
+
// Send email verification
|
|
51
|
+
await sendEmailVerification(result.user)
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw this.handleAuthError(error)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async signInWithGoogle(): Promise<UserCredential> {
|
|
60
|
+
try {
|
|
61
|
+
const provider = new GoogleAuthProvider()
|
|
62
|
+
provider.addScope('profile')
|
|
63
|
+
provider.addScope('email')
|
|
64
|
+
|
|
65
|
+
return await signInWithPopup(this.auth, provider)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw this.handleAuthError(error)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async signOut(): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
await firebaseSignOut(this.auth)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw createAuthError(AuthErrorCode.SIGN_OUT_FAILED, 'Sign out failed', error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async sendPasswordReset(email: string): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
await sendPasswordResetEmail(this.auth, email)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw this.handleAuthError(error)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async resendEmailVerification(): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
const user = this.auth.currentUser
|
|
90
|
+
if (!user) {
|
|
91
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
92
|
+
}
|
|
93
|
+
await sendEmailVerification(user)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw createAuthError(AuthErrorCode.EMAIL_VERIFICATION_FAILED, 'Failed to resend verification', error)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Profile Management
|
|
100
|
+
|
|
101
|
+
async updateProfile(
|
|
102
|
+
updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL'>>
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
const user = this.auth.currentUser
|
|
106
|
+
if (!user) {
|
|
107
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await updateAuthProfile(user, updates)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Profile update failed', error)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async updateEmail(newEmail: string, password: string): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
const user = this.auth.currentUser
|
|
119
|
+
if (!user || !user.email) {
|
|
120
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const credential = EmailAuthProvider.credential(user.email, password)
|
|
124
|
+
await reauthenticateWithCredential(user, credential)
|
|
125
|
+
await updateAuthEmail(user, newEmail)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw createAuthError(AuthErrorCode.EMAIL_UPDATE_FAILED, 'Email update failed', error)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async updatePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const user = this.auth.currentUser
|
|
134
|
+
if (!user || !user.email) {
|
|
135
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const credential = EmailAuthProvider.credential(user.email, currentPassword)
|
|
139
|
+
await reauthenticateWithCredential(user, credential)
|
|
140
|
+
await updateAuthPassword(user, newPassword)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw createAuthError(AuthErrorCode.PASSWORD_UPDATE_FAILED, 'Password update failed', error)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async deleteAccount(password: string): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
const user = this.auth.currentUser
|
|
149
|
+
if (!user || !user.email) {
|
|
150
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const credential = EmailAuthProvider.credential(user.email, password)
|
|
154
|
+
await reauthenticateWithCredential(user, credential)
|
|
155
|
+
await user.delete()
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Account deletion failed', error)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// State Management
|
|
162
|
+
|
|
163
|
+
getCurrentUser(): FirebaseUser | null {
|
|
164
|
+
return this.auth.currentUser
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
onAuthStateChanged(callback: (user: FirebaseUser | null) => void) {
|
|
168
|
+
return this.auth.onAuthStateChanged(callback)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Note: User document operations should be handled by UserAdapter
|
|
172
|
+
// These methods are part of IAuthRepository interface but should be implemented separately
|
|
173
|
+
async createUserDocument(
|
|
174
|
+
_userId: string,
|
|
175
|
+
_data: Partial<Omit<User, 'profile'>> & { email: string; displayName: string }
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
throw new Error('createUserDocument should be handled by UserAdapter')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async updateLastLogin(_userId: string): Promise<void> {
|
|
181
|
+
throw new Error('updateLastLogin should be handled by UserAdapter')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle Firebase Auth errors
|
|
186
|
+
*/
|
|
187
|
+
private handleAuthError(error: unknown): Error {
|
|
188
|
+
if (error instanceof Error && 'code' in error) {
|
|
189
|
+
const code = (error as { code: string }).code
|
|
190
|
+
|
|
191
|
+
switch (code) {
|
|
192
|
+
case 'auth/user-not-found':
|
|
193
|
+
return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'User not found', error)
|
|
194
|
+
case 'auth/wrong-password':
|
|
195
|
+
case 'auth/invalid-credential':
|
|
196
|
+
return createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials', error)
|
|
197
|
+
case 'auth/email-already-in-use':
|
|
198
|
+
return createAuthError(AuthErrorCode.USER_ALREADY_EXISTS, 'Email already in use', error)
|
|
199
|
+
case 'auth/weak-password':
|
|
200
|
+
return createAuthError(AuthErrorCode.WEAK_PASSWORD, 'Password is too weak', error)
|
|
201
|
+
case 'auth/invalid-email':
|
|
202
|
+
return createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid email', error)
|
|
203
|
+
case 'auth/user-disabled':
|
|
204
|
+
return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'Account disabled', error)
|
|
205
|
+
case 'auth/too-many-requests':
|
|
206
|
+
return createAuthError(AuthErrorCode.TOO_MANY_REQUESTS, 'Too many requests', error)
|
|
207
|
+
case 'auth/popup-closed-by-user':
|
|
208
|
+
return createAuthError(AuthErrorCode.OAUTH_CANCELLED, 'Sign in cancelled', error)
|
|
209
|
+
case 'auth/account-exists-with-different-credential':
|
|
210
|
+
return createAuthError(AuthErrorCode.OAUTH_ACCOUNT_EXISTS, 'Account exists with different provider', error)
|
|
211
|
+
case 'auth/requires-recent-login':
|
|
212
|
+
return createAuthError(AuthErrorCode.REAUTHENTICATION_REQUIRED, 'Please reauthenticate', error)
|
|
213
|
+
default:
|
|
214
|
+
return createAuthError(AuthErrorCode.UNKNOWN, `Auth error: ${code}`, error)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return createAuthError(AuthErrorCode.UNKNOWN, 'Unknown auth error', error)
|
|
219
|
+
}
|
|
220
|
+
}
|