@umituz/web-firebase 1.0.4 → 2.0.1
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 +39 -12
- 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/firebase.entity.ts +13 -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 +30 -0
- package/src/domain/interfaces/auth.repository.interface.ts +83 -0
- package/src/domain/interfaces/file.repository.interface.ts +143 -0
- package/src/domain/interfaces/repository.interface.ts +11 -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 +23 -0
- 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 +13 -0
- package/src/infrastructure/services/firebase.service.ts +30 -0
- package/src/infrastructure/services/firestore.repository.ts +66 -0
- package/src/infrastructure/utils/storage.util.ts +46 -0
- package/src/presentation/hooks/useAuth.ts +153 -0
- package/src/presentation/hooks/useFirebaseAuth.ts +122 -0
- package/src/presentation/hooks/useFirestore.ts +125 -0
- package/src/presentation/hooks/useStorage.ts +141 -0
- package/src/presentation/index.ts +6 -0
- package/src/presentation/providers/FirebaseProvider.tsx +40 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Client
|
|
3
|
+
* @description Firebase initialization and singleton instances
|
|
4
|
+
* Migrated from: /Users/umituz/Desktop/github/umituz/apps/web/app-growth-factory/src/domains/firebase/services/client.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { initializeApp, getApps, FirebaseApp } from 'firebase/app'
|
|
8
|
+
import { initializeAuth, getAuth, Auth, browserLocalPersistence } from 'firebase/auth'
|
|
9
|
+
import { getFirestore, Firestore } from 'firebase/firestore'
|
|
10
|
+
import { getStorage, FirebaseStorage } from 'firebase/storage'
|
|
11
|
+
import { getAnalytics, Analytics } from 'firebase/analytics'
|
|
12
|
+
import { getFunctions, Functions } from 'firebase/functions'
|
|
13
|
+
|
|
14
|
+
const firebaseConfig = {
|
|
15
|
+
apiKey: process.env.VITE_FIREBASE_API_KEY,
|
|
16
|
+
authDomain: process.env.VITE_FIREBASE_AUTH_DOMAIN,
|
|
17
|
+
projectId: process.env.VITE_FIREBASE_PROJECT_ID,
|
|
18
|
+
storageBucket: process.env.VITE_FIREBASE_STORAGE_BUCKET,
|
|
19
|
+
messagingSenderId: process.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
|
20
|
+
appId: process.env.VITE_FIREBASE_APP_ID,
|
|
21
|
+
measurementId: process.env.VITE_FIREBASE_MEASUREMENT_ID,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Singleton instances
|
|
25
|
+
let app: FirebaseApp
|
|
26
|
+
let auth: Auth
|
|
27
|
+
let db: Firestore
|
|
28
|
+
let storage: FirebaseStorage
|
|
29
|
+
let functions: Functions
|
|
30
|
+
let analytics: Analytics | null = null
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize Firebase App
|
|
34
|
+
*/
|
|
35
|
+
export function initializeFirebase(): FirebaseApp {
|
|
36
|
+
if (!getApps().length) {
|
|
37
|
+
app = initializeApp(firebaseConfig)
|
|
38
|
+
} else {
|
|
39
|
+
app = getApps()[0]
|
|
40
|
+
}
|
|
41
|
+
return app
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get Firebase App instance
|
|
46
|
+
*/
|
|
47
|
+
export function getFirebaseApp(): FirebaseApp {
|
|
48
|
+
return app || initializeFirebase()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get Firebase Auth instance
|
|
53
|
+
*/
|
|
54
|
+
export function getFirebaseAuth(): Auth {
|
|
55
|
+
if (!auth) {
|
|
56
|
+
const firebaseApp = getFirebaseApp()
|
|
57
|
+
if (typeof window !== 'undefined') {
|
|
58
|
+
try {
|
|
59
|
+
auth = getAuth(firebaseApp)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.warn('getAuth failed, trying initializeAuth...', e)
|
|
62
|
+
auth = initializeAuth(firebaseApp, {
|
|
63
|
+
persistence: browserLocalPersistence,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
auth = getAuth(firebaseApp)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return auth
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get Firestore instance
|
|
75
|
+
*/
|
|
76
|
+
export function getFirebaseDB(): Firestore {
|
|
77
|
+
if (!db) {
|
|
78
|
+
db = getFirestore(getFirebaseApp())
|
|
79
|
+
}
|
|
80
|
+
return db
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get Firebase Storage instance
|
|
85
|
+
*/
|
|
86
|
+
export function getFirebaseStorage(): FirebaseStorage {
|
|
87
|
+
if (!storage) {
|
|
88
|
+
storage = getStorage(getFirebaseApp())
|
|
89
|
+
}
|
|
90
|
+
return storage
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get Firebase Functions instance
|
|
95
|
+
*/
|
|
96
|
+
export function getFirebaseFunctions(): Functions {
|
|
97
|
+
if (!functions) {
|
|
98
|
+
functions = getFunctions(getFirebaseApp())
|
|
99
|
+
}
|
|
100
|
+
return functions
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get Firebase Analytics instance
|
|
105
|
+
*/
|
|
106
|
+
export function getFirebaseAnalytics(): Analytics | null {
|
|
107
|
+
if (!analytics && typeof window !== 'undefined') {
|
|
108
|
+
analytics = getAnalytics(getFirebaseApp())
|
|
109
|
+
}
|
|
110
|
+
return analytics
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Firebase Instances
|
|
115
|
+
* All Firebase service instances
|
|
116
|
+
*/
|
|
117
|
+
export interface FirebaseInstances {
|
|
118
|
+
app: FirebaseApp
|
|
119
|
+
auth: Auth
|
|
120
|
+
db: Firestore
|
|
121
|
+
storage: FirebaseStorage
|
|
122
|
+
functions: Functions
|
|
123
|
+
analytics: Analytics | null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all Firebase instances
|
|
128
|
+
*/
|
|
129
|
+
export function getFirebaseInstances(): FirebaseInstances {
|
|
130
|
+
return {
|
|
131
|
+
app: getFirebaseApp(),
|
|
132
|
+
auth: getFirebaseAuth(),
|
|
133
|
+
db: getFirebaseDB(),
|
|
134
|
+
storage: getFirebaseStorage(),
|
|
135
|
+
functions: getFirebaseFunctions(),
|
|
136
|
+
analytics: getFirebaseAnalytics(),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Export singleton instances for convenience
|
|
141
|
+
export { app, auth, db, storage, functions, analytics }
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Adapter
|
|
3
|
+
* @description Firebase Firestore implementation of IUserRepository
|
|
4
|
+
* Migrated from: /Users/umituz/Desktop/github/umituz/apps/web/app-growth-factory/src/domains/firebase/services/firestore.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
doc,
|
|
9
|
+
getDoc,
|
|
10
|
+
setDoc,
|
|
11
|
+
updateDoc,
|
|
12
|
+
deleteDoc,
|
|
13
|
+
query,
|
|
14
|
+
where,
|
|
15
|
+
onSnapshot,
|
|
16
|
+
collection,
|
|
17
|
+
getDocs,
|
|
18
|
+
} from 'firebase/firestore'
|
|
19
|
+
import { getFirebaseDB } from './client'
|
|
20
|
+
import type { IUserRepository } from '../../domain/interfaces/user.repository.interface'
|
|
21
|
+
import type { User } from '../../domain/entities/user.entity'
|
|
22
|
+
import { createRepositoryError, RepositoryErrorCode } from '../../domain/errors/repository.errors'
|
|
23
|
+
|
|
24
|
+
export class FirestoreAdapter implements IUserRepository {
|
|
25
|
+
private get db() {
|
|
26
|
+
return getFirebaseDB()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private readonly USERS_COLLECTION = 'users'
|
|
30
|
+
|
|
31
|
+
async getUser(userId: string): Promise<User | null> {
|
|
32
|
+
try {
|
|
33
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
34
|
+
const snap = await getDoc(docRef)
|
|
35
|
+
|
|
36
|
+
if (!snap.exists()) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return snap.data() as User
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'User not found', error)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getUserByEmail(email: string): Promise<User | null> {
|
|
47
|
+
try {
|
|
48
|
+
const q = query(collection(this.db, this.USERS_COLLECTION), where('profile.email', '==', email))
|
|
49
|
+
const snap = await getDocs(q)
|
|
50
|
+
|
|
51
|
+
if (snap.empty) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const doc = snap.docs[0]
|
|
56
|
+
return doc.data() as User
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw createRepositoryError(RepositoryErrorCode.QUERY_FAILED, 'Failed to query user', error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async createUser(userId: string, data: Partial<User>): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
65
|
+
await setDoc(docRef, data, { merge: true })
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_INVALID, 'Failed to create user', error)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async updateUser(userId: string, data: Partial<User>): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
74
|
+
await updateDoc(docRef, {
|
|
75
|
+
...data,
|
|
76
|
+
'profile.updatedAt': Date.now(),
|
|
77
|
+
} as any)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to update user', error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deleteUser(userId: string): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
86
|
+
await deleteDoc(docRef)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to delete user', error)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateProfile(
|
|
93
|
+
userId: string,
|
|
94
|
+
updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL' | 'phoneNumber'>>
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
98
|
+
const updateData: any = {
|
|
99
|
+
'profile.updatedAt': Date.now(),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (updates.displayName !== undefined) {
|
|
103
|
+
updateData['profile.displayName'] = updates.displayName
|
|
104
|
+
}
|
|
105
|
+
if (updates.photoURL !== undefined) {
|
|
106
|
+
updateData['profile.photoURL'] = updates.photoURL
|
|
107
|
+
}
|
|
108
|
+
if (updates.phoneNumber !== undefined) {
|
|
109
|
+
updateData['profile.phoneNumber'] = updates.phoneNumber
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await updateDoc(docRef, updateData)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to update profile', error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async updateSettings(userId: string, settings: Partial<User['settings']>): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
121
|
+
await updateDoc(docRef, {
|
|
122
|
+
settings: {
|
|
123
|
+
...settings,
|
|
124
|
+
updatedAt: Date.now(),
|
|
125
|
+
},
|
|
126
|
+
} as any)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to update settings', error)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async updateSubscription(userId: string, subscription: Partial<User['subscription']>): Promise<void> {
|
|
133
|
+
try {
|
|
134
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
135
|
+
await updateDoc(docRef, {
|
|
136
|
+
subscription: {
|
|
137
|
+
...subscription,
|
|
138
|
+
updatedAt: Date.now(),
|
|
139
|
+
},
|
|
140
|
+
} as any)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to update subscription', error)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async updateLastLogin(userId: string): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
149
|
+
await updateDoc(docRef, {
|
|
150
|
+
'profile.lastLoginAt': Date.now(),
|
|
151
|
+
} as any)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw createRepositoryError(RepositoryErrorCode.DOCUMENT_NOT_FOUND, 'Failed to update last login', error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async queryUsers(constraints: any[]): Promise<User[]> {
|
|
158
|
+
try {
|
|
159
|
+
const q = query(collection(this.db, this.USERS_COLLECTION), ...constraints)
|
|
160
|
+
const snap = await getDocs(q)
|
|
161
|
+
return snap.docs.map((doc) => doc.data() as User)
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw createRepositoryError(RepositoryErrorCode.QUERY_FAILED, 'Failed to query users', error)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
subscribeToUser(
|
|
168
|
+
userId: string,
|
|
169
|
+
callback: (user: User | null) => void,
|
|
170
|
+
onError?: (error: Error) => void
|
|
171
|
+
): () => void {
|
|
172
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
173
|
+
|
|
174
|
+
const unsubscribe = onSnapshot(
|
|
175
|
+
docRef,
|
|
176
|
+
(snap) => {
|
|
177
|
+
if (snap.exists()) {
|
|
178
|
+
callback(snap.data() as User)
|
|
179
|
+
} else {
|
|
180
|
+
callback(null)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
(error) => {
|
|
184
|
+
onError?.(error as Error)
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return unsubscribe
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Adapter
|
|
3
|
+
* @description Firebase Storage implementation of IFileRepository
|
|
4
|
+
* Migrated from: /Users/umituz/Desktop/github/umituz/apps/web/app-growth-factory/src/domains/firebase/services/storage.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ref,
|
|
9
|
+
uploadBytes,
|
|
10
|
+
uploadBytesResumable,
|
|
11
|
+
getDownloadURL,
|
|
12
|
+
deleteObject,
|
|
13
|
+
listAll,
|
|
14
|
+
getMetadata,
|
|
15
|
+
} from 'firebase/storage'
|
|
16
|
+
import { getFirebaseStorage } from './client'
|
|
17
|
+
import type { IFileRepository } from '../../domain/interfaces/file.repository.interface'
|
|
18
|
+
import type {
|
|
19
|
+
FileMetadata,
|
|
20
|
+
UploadProgress,
|
|
21
|
+
UploadResult,
|
|
22
|
+
UploadOptions,
|
|
23
|
+
StorageStats,
|
|
24
|
+
} from '../../domain/entities/file.entity'
|
|
25
|
+
import { createRepositoryError, RepositoryErrorCode } from '../../domain/errors/repository.errors'
|
|
26
|
+
|
|
27
|
+
export class StorageAdapter implements IFileRepository {
|
|
28
|
+
private get storage() {
|
|
29
|
+
return getFirebaseStorage()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Upload Methods
|
|
33
|
+
|
|
34
|
+
async uploadFile(
|
|
35
|
+
userId: string,
|
|
36
|
+
path: string,
|
|
37
|
+
file: File | Blob,
|
|
38
|
+
options?: UploadOptions
|
|
39
|
+
): Promise<UploadResult> {
|
|
40
|
+
const storageRef = ref(this.storage, `users/${userId}/${path}`)
|
|
41
|
+
const uploadTask = uploadBytesResumable(storageRef, file)
|
|
42
|
+
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
uploadTask.on(
|
|
45
|
+
'state_changed',
|
|
46
|
+
(snapshot) => {
|
|
47
|
+
if (options?.onProgress) {
|
|
48
|
+
const progress: UploadProgress = {
|
|
49
|
+
bytesTransferred: snapshot.bytesTransferred,
|
|
50
|
+
totalBytes: snapshot.totalBytes,
|
|
51
|
+
progress: (snapshot.bytesTransferred / snapshot.totalBytes) * 100,
|
|
52
|
+
state: 'running',
|
|
53
|
+
}
|
|
54
|
+
options.onProgress(progress)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
(error) => reject(createRepositoryError(RepositoryErrorCode.STORAGE_ERROR, 'Upload failed', error)),
|
|
58
|
+
async () => {
|
|
59
|
+
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref)
|
|
60
|
+
const metadata = await getMetadata(uploadTask.snapshot.ref)
|
|
61
|
+
|
|
62
|
+
resolve({
|
|
63
|
+
id: uploadTask.snapshot.ref.name,
|
|
64
|
+
name: metadata.name || uploadTask.snapshot.ref.name,
|
|
65
|
+
fullPath: metadata.fullPath || uploadTask.snapshot.ref.fullPath,
|
|
66
|
+
downloadURL,
|
|
67
|
+
contentType: metadata.contentType || '',
|
|
68
|
+
size: metadata.size || 0,
|
|
69
|
+
createdAt: metadata.timeCreated ? new Date(metadata.timeCreated).getTime() : Date.now(),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async uploadImage(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
77
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
78
|
+
return this.uploadFile(userId, `images/${name}`, file)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async uploadVideo(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
82
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
83
|
+
return this.uploadFile(userId, `videos/${name}`, file)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async uploadDocument(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
87
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
88
|
+
return this.uploadFile(userId, `documents/${name}`, file)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async uploadProfilePicture(userId: string, file: File): Promise<UploadResult> {
|
|
92
|
+
const storageRef = ref(this.storage, `users/${userId}/profile/${Date.now()}_${file.name}`)
|
|
93
|
+
await uploadBytes(storageRef, file)
|
|
94
|
+
const downloadURL = await getDownloadURL(storageRef)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: storageRef.name,
|
|
98
|
+
name: file.name,
|
|
99
|
+
fullPath: storageRef.fullPath,
|
|
100
|
+
downloadURL,
|
|
101
|
+
contentType: file.type,
|
|
102
|
+
size: file.size,
|
|
103
|
+
createdAt: Date.now(),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Download Methods
|
|
108
|
+
|
|
109
|
+
async getDownloadURL(path: string): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const storageRef = ref(this.storage, path)
|
|
112
|
+
return await getDownloadURL(storageRef)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw createRepositoryError(RepositoryErrorCode.FILE_NOT_FOUND, 'File not found', error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Delete Methods
|
|
119
|
+
|
|
120
|
+
async deleteFile(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
const storageRef = ref(this.storage, path)
|
|
123
|
+
await deleteObject(storageRef)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw createRepositoryError(RepositoryErrorCode.FILE_NOT_FOUND, 'File not found', error)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async deleteUserFiles(userId: string): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
const userRef = ref(this.storage, `users/${userId}`)
|
|
132
|
+
const result = await listAll(userRef)
|
|
133
|
+
|
|
134
|
+
// Delete all files in all prefixes
|
|
135
|
+
for (const prefix of result.prefixes) {
|
|
136
|
+
const prefixResult = await listAll(prefix)
|
|
137
|
+
await Promise.all(prefixResult.items.map((item) => deleteObject(item)))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Delete all files in root
|
|
141
|
+
await Promise.all(result.items.map((item) => deleteObject(item)))
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw createRepositoryError(RepositoryErrorCode.STORAGE_ERROR, 'Failed to delete user files', error)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async deleteImage(userId: string, filename: string): Promise<void> {
|
|
148
|
+
await this.deleteFile(`users/${userId}/images/${filename}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteVideo(userId: string, filename: string): Promise<void> {
|
|
152
|
+
await this.deleteFile(`users/${userId}/videos/${filename}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async deleteProfilePicture(userId: string, filename: string): Promise<void> {
|
|
156
|
+
await this.deleteFile(`users/${userId}/profile/${filename}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// List Methods
|
|
160
|
+
|
|
161
|
+
async listUserFiles(userId: string, path?: string): Promise<string[]> {
|
|
162
|
+
const userRef = ref(this.storage, path ? `users/${userId}/${path}` : `users/${userId}`)
|
|
163
|
+
const result = await listAll(userRef)
|
|
164
|
+
|
|
165
|
+
const urls = await Promise.all(result.items.map((item) => getDownloadURL(item)))
|
|
166
|
+
return urls
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async listUserImages(userId: string): Promise<string[]> {
|
|
170
|
+
return this.listUserFiles(userId, 'images')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async listUserVideos(userId: string): Promise<string[]> {
|
|
174
|
+
return this.listUserFiles(userId, 'videos')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Metadata
|
|
178
|
+
|
|
179
|
+
async getFileMetadata(path: string): Promise<FileMetadata> {
|
|
180
|
+
try {
|
|
181
|
+
const storageRef = ref(this.storage, path)
|
|
182
|
+
const metadata = await getMetadata(storageRef)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
id: storageRef.name,
|
|
186
|
+
name: metadata.name,
|
|
187
|
+
fullPath: metadata.fullPath,
|
|
188
|
+
contentType: metadata.contentType || 'application/octet-stream',
|
|
189
|
+
size: metadata.size,
|
|
190
|
+
createdAt: metadata.timeCreated ? new Date(metadata.timeCreated).getTime() : Date.now(),
|
|
191
|
+
updatedAt: metadata.updated ? new Date(metadata.updated).getTime() : Date.now(),
|
|
192
|
+
userId: this.extractUserId(path) || 'unknown',
|
|
193
|
+
type: this.extractFileType(metadata.contentType || ''),
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw createRepositoryError(RepositoryErrorCode.FILE_NOT_FOUND, 'File not found', error)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async queryFiles(userId: string, _filters?: any): Promise<{ files: FileMetadata[]; totalCount: number; hasMore: boolean }> {
|
|
201
|
+
const userRef = ref(this.storage, `users/${userId}`)
|
|
202
|
+
const result = await listAll(userRef)
|
|
203
|
+
|
|
204
|
+
const files = await Promise.all(
|
|
205
|
+
result.items.map(async (item) => {
|
|
206
|
+
const metadata = await getMetadata(item)
|
|
207
|
+
return {
|
|
208
|
+
id: item.name,
|
|
209
|
+
name: metadata.name,
|
|
210
|
+
fullPath: metadata.fullPath,
|
|
211
|
+
contentType: metadata.contentType || 'application/octet-stream',
|
|
212
|
+
size: metadata.size,
|
|
213
|
+
createdAt: metadata.timeCreated ? new Date(metadata.timeCreated).getTime() : Date.now(),
|
|
214
|
+
updatedAt: metadata.updated ? new Date(metadata.updated).getTime() : Date.now(),
|
|
215
|
+
userId,
|
|
216
|
+
type: this.extractFileType(metadata.contentType || ''),
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
files,
|
|
223
|
+
totalCount: files.length,
|
|
224
|
+
hasMore: false,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getStorageStats(userId: string): Promise<StorageStats> {
|
|
229
|
+
const { files, totalCount } = await this.queryFiles(userId)
|
|
230
|
+
|
|
231
|
+
const stats: StorageStats = {
|
|
232
|
+
totalFiles: totalCount,
|
|
233
|
+
totalSize: files.reduce((sum, file) => sum + file.size, 0),
|
|
234
|
+
filesByType: {
|
|
235
|
+
image: 0,
|
|
236
|
+
video: 0,
|
|
237
|
+
audio: 0,
|
|
238
|
+
document: 0,
|
|
239
|
+
other: 0,
|
|
240
|
+
},
|
|
241
|
+
filesByCategory: {
|
|
242
|
+
profile: 0,
|
|
243
|
+
content: 0,
|
|
244
|
+
document: 0,
|
|
245
|
+
attachment: 0,
|
|
246
|
+
backup: 0,
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
files.forEach((file) => {
|
|
251
|
+
stats.filesByType[file.type]++
|
|
252
|
+
stats.lastUploadAt = Math.max(stats.lastUploadAt || 0, file.createdAt)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return stats
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Validation
|
|
259
|
+
|
|
260
|
+
validateFile(file: File, options?: { maxSizeBytes?: number; maxSizeMB?: number; allowedTypes?: string[] }): boolean {
|
|
261
|
+
const maxSizeBytes = options?.maxSizeBytes || (options?.maxSizeMB ? options.maxSizeMB * 1024 * 1024 : 10 * 1024 * 1024)
|
|
262
|
+
|
|
263
|
+
if (file.size > maxSizeBytes) {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (options?.allowedTypes && !options.allowedTypes.includes(file.type)) {
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
isImageFile(file: File): boolean {
|
|
275
|
+
return file.type.startsWith('image/')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
isVideoFile(file: File): boolean {
|
|
279
|
+
return file.type.startsWith('video/')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
isDocumentFile(file: File): boolean {
|
|
283
|
+
const docTypes = [
|
|
284
|
+
'application/pdf',
|
|
285
|
+
'application/msword',
|
|
286
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
287
|
+
'application/vnd.ms-excel',
|
|
288
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
289
|
+
'text/plain',
|
|
290
|
+
]
|
|
291
|
+
return docTypes.includes(file.type)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Utility Methods
|
|
295
|
+
|
|
296
|
+
generateUniqueFilename(originalName: string): string {
|
|
297
|
+
const timestamp = Date.now()
|
|
298
|
+
const random = Math.random().toString(36).substring(2, 8)
|
|
299
|
+
const extension = this.getFileExtension(originalName)
|
|
300
|
+
return `${timestamp}_${random}.${extension}`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getFileExtension(filename: string): string {
|
|
304
|
+
return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Helper Methods
|
|
308
|
+
|
|
309
|
+
private extractUserId(path: string): string {
|
|
310
|
+
const match = path.match(/users\/([^\/]+)/)
|
|
311
|
+
return match ? match[1] : ''
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private extractFileType(contentType: string): 'image' | 'video' | 'audio' | 'document' | 'other' {
|
|
315
|
+
if (contentType.startsWith('image/')) return 'image'
|
|
316
|
+
if (contentType.startsWith('video/')) return 'video'
|
|
317
|
+
if (contentType.startsWith('audio/')) return 'audio'
|
|
318
|
+
if (contentType.includes('pdf') || contentType.includes('document') || contentType.includes('text')) {
|
|
319
|
+
return 'document'
|
|
320
|
+
}
|
|
321
|
+
return 'other'
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer Public API
|
|
3
|
+
* @description Exports all Firebase adapters and repositories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Firebase
|
|
7
|
+
export * from './firebase/client'
|
|
8
|
+
export * from './firebase/auth.adapter'
|
|
9
|
+
export * from './firebase/firestore.adapter'
|
|
10
|
+
export * from './firebase/storage.adapter'
|
|
11
|
+
|
|
12
|
+
// Utils
|
|
13
|
+
export * from './utils/storage.util'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { initializeApp, getApps, type FirebaseOptions, type FirebaseApp } from 'firebase/app';
|
|
2
|
+
import { getAuth, type Auth } from 'firebase/auth';
|
|
3
|
+
import { getFirestore, type Firestore } from 'firebase/firestore';
|
|
4
|
+
import { getStorage, type FirebaseStorage } from 'firebase/storage';
|
|
5
|
+
import { getFunctions, type Functions } from 'firebase/functions';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Firebase Service Infrastructure
|
|
9
|
+
* @description Initialization and instance management for Firebase
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface FirebaseInstances {
|
|
13
|
+
app: FirebaseApp;
|
|
14
|
+
auth: Auth;
|
|
15
|
+
db: Firestore;
|
|
16
|
+
storage: FirebaseStorage;
|
|
17
|
+
functions: Functions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function initializeFirebase(config: FirebaseOptions, appName?: string): FirebaseInstances {
|
|
21
|
+
const existing = getApps().find((a) => a.name === (appName ?? '[DEFAULT]'));
|
|
22
|
+
const app = existing ?? initializeApp(config, appName);
|
|
23
|
+
return {
|
|
24
|
+
app,
|
|
25
|
+
auth: getAuth(app),
|
|
26
|
+
db: getFirestore(app),
|
|
27
|
+
storage: getStorage(app),
|
|
28
|
+
functions: getFunctions(app),
|
|
29
|
+
};
|
|
30
|
+
}
|