@umituz/web-firebase 2.1.0 → 3.0.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/package.json +12 -39
- package/src/domains/auth/entities/index.ts +60 -0
- package/src/domains/auth/index.ts +13 -0
- package/src/domains/auth/services/auth.service.ts +245 -0
- package/src/domains/auth/services/index.ts +7 -0
- package/src/domains/auth/types/auth-service.interface.ts +72 -0
- package/src/domains/auth/types/index.ts +5 -0
- package/src/domains/firestore/entities/index.ts +82 -0
- package/src/domains/firestore/index.ts +13 -0
- package/src/domains/firestore/services/firestore.service.ts +191 -0
- package/src/domains/firestore/services/index.ts +7 -0
- package/src/domains/firestore/types/firestore-service.interface.ts +64 -0
- package/src/domains/firestore/types/index.ts +5 -0
- package/src/domains/storage/entities/index.ts +94 -0
- package/src/domains/storage/index.ts +13 -0
- package/src/domains/storage/services/index.ts +7 -0
- package/src/domains/storage/services/storage.service.ts +223 -0
- package/src/domains/storage/types/index.ts +5 -0
- package/src/domains/storage/types/storage-service.interface.ts +120 -0
- package/src/index.ts +12 -16
- package/src/presentation/hooks/useAuth.ts +69 -26
- package/src/presentation/providers/FirebaseProvider.tsx +9 -14
- package/dist/application/index.d.mts +0 -273
- package/dist/application/index.d.ts +0 -273
- package/dist/application/index.js +0 -490
- package/dist/application/index.mjs +0 -19
- package/dist/chunk-34DL2QWQ.mjs +0 -87
- package/dist/chunk-4FP2ELQ5.mjs +0 -96
- package/dist/chunk-7TX3OU3O.mjs +0 -721
- package/dist/chunk-I6WGBPFB.mjs +0 -439
- package/dist/chunk-RZ4QR6TB.mjs +0 -96
- package/dist/chunk-U2XI4MGO.mjs +0 -397
- package/dist/domain/index.d.mts +0 -325
- package/dist/domain/index.d.ts +0 -325
- package/dist/domain/index.js +0 -662
- package/dist/domain/index.mjs +0 -36
- package/dist/file.repository.interface-v5vHgVsZ.d.mts +0 -241
- package/dist/file.repository.interface-v5vHgVsZ.d.ts +0 -241
- package/dist/firebase.entity-xvfEPjXZ.d.mts +0 -15
- package/dist/firebase.entity-xvfEPjXZ.d.ts +0 -15
- package/dist/index.d.mts +0 -14
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -1833
- package/dist/index.mjs +0 -98
- package/dist/infrastructure/index.d.mts +0 -170
- package/dist/infrastructure/index.d.ts +0 -170
- package/dist/infrastructure/index.js +0 -856
- package/dist/infrastructure/index.mjs +0 -46
- package/dist/presentation/index.d.mts +0 -25
- package/dist/presentation/index.d.ts +0 -25
- package/dist/presentation/index.js +0 -105
- package/dist/presentation/index.mjs +0 -6
- package/dist/user.repository.interface-DS74TsJ5.d.mts +0 -298
- package/dist/user.repository.interface-DS74TsJ5.d.ts +0 -298
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Service
|
|
3
|
+
* @description Firebase Firestore implementation of IFirestoreService
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
doc,
|
|
8
|
+
getDoc,
|
|
9
|
+
setDoc,
|
|
10
|
+
updateDoc,
|
|
11
|
+
deleteDoc,
|
|
12
|
+
query,
|
|
13
|
+
where,
|
|
14
|
+
onSnapshot,
|
|
15
|
+
collection,
|
|
16
|
+
getDocs,
|
|
17
|
+
} from 'firebase/firestore'
|
|
18
|
+
import { getFirebaseDB } from '../../../infrastructure/firebase/client'
|
|
19
|
+
import type { IFirestoreService } from '../types'
|
|
20
|
+
import type { User } from '../entities'
|
|
21
|
+
|
|
22
|
+
class FirestoreService implements IFirestoreService {
|
|
23
|
+
private get db() {
|
|
24
|
+
return getFirebaseDB()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private readonly USERS_COLLECTION = 'users'
|
|
28
|
+
|
|
29
|
+
async getUser(userId: string): Promise<User | null> {
|
|
30
|
+
try {
|
|
31
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
32
|
+
const snap = await getDoc(docRef)
|
|
33
|
+
|
|
34
|
+
if (!snap.exists()) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return snap.data() as User
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error('User not found')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getUserByEmail(email: string): Promise<User | null> {
|
|
45
|
+
try {
|
|
46
|
+
const q = query(collection(this.db, this.USERS_COLLECTION), where('profile.email', '==', email))
|
|
47
|
+
const snap = await getDocs(q)
|
|
48
|
+
|
|
49
|
+
if (snap.empty) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const doc = snap.docs[0]
|
|
54
|
+
return doc.data() as User
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error('Failed to query user')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async createUser(userId: string, data: Partial<User>): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
63
|
+
await setDoc(docRef, data, { merge: true })
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error('Failed to create user')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async updateUser(userId: string, data: Partial<User>): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
72
|
+
await updateDoc(docRef, {
|
|
73
|
+
...data,
|
|
74
|
+
'profile.updatedAt': Date.now(),
|
|
75
|
+
} as any)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error('Failed to update user')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async deleteUser(userId: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
84
|
+
await deleteDoc(docRef)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error('Failed to delete user')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async updateProfile(
|
|
91
|
+
userId: string,
|
|
92
|
+
updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL' | 'phoneNumber'>>
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
96
|
+
const updateData: any = {
|
|
97
|
+
'profile.updatedAt': Date.now(),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (updates.displayName !== undefined) {
|
|
101
|
+
updateData['profile.displayName'] = updates.displayName
|
|
102
|
+
}
|
|
103
|
+
if (updates.photoURL !== undefined) {
|
|
104
|
+
updateData['profile.photoURL'] = updates.photoURL
|
|
105
|
+
}
|
|
106
|
+
if (updates.phoneNumber !== undefined) {
|
|
107
|
+
updateData['profile.phoneNumber'] = updates.phoneNumber
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await updateDoc(docRef, updateData)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error('Failed to update profile')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async updateSettings(userId: string, settings: Partial<User['settings']>): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
119
|
+
await updateDoc(docRef, {
|
|
120
|
+
settings: {
|
|
121
|
+
...settings,
|
|
122
|
+
updatedAt: Date.now(),
|
|
123
|
+
},
|
|
124
|
+
} as any)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error('Failed to update settings')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async updateSubscription(userId: string, subscription: Partial<User['subscription']>): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
133
|
+
await updateDoc(docRef, {
|
|
134
|
+
subscription: {
|
|
135
|
+
...subscription,
|
|
136
|
+
updatedAt: Date.now(),
|
|
137
|
+
},
|
|
138
|
+
} as any)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new Error('Failed to update subscription')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async updateLastLogin(userId: string): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
147
|
+
await updateDoc(docRef, {
|
|
148
|
+
'profile.lastLoginAt': Date.now(),
|
|
149
|
+
} as any)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error('Failed to update last login')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async queryUsers(constraints: any[]): Promise<User[]> {
|
|
156
|
+
try {
|
|
157
|
+
const q = query(collection(this.db, this.USERS_COLLECTION), ...constraints)
|
|
158
|
+
const snap = await getDocs(q)
|
|
159
|
+
return snap.docs.map((doc) => doc.data() as User)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error('Failed to query users')
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
subscribeToUser(
|
|
166
|
+
userId: string,
|
|
167
|
+
callback: (user: User | null) => void,
|
|
168
|
+
onError?: (error: Error) => void
|
|
169
|
+
): () => void {
|
|
170
|
+
const docRef = doc(this.db, this.USERS_COLLECTION, userId)
|
|
171
|
+
|
|
172
|
+
const unsubscribe = onSnapshot(
|
|
173
|
+
docRef,
|
|
174
|
+
(snap) => {
|
|
175
|
+
if (snap.exists()) {
|
|
176
|
+
callback(snap.data() as User)
|
|
177
|
+
} else {
|
|
178
|
+
callback(null)
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
(error) => {
|
|
182
|
+
onError?.(error as Error)
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return unsubscribe
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Export class and singleton instance
|
|
191
|
+
export const firestoreService = new FirestoreService()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Service Interface
|
|
3
|
+
* @description Abstract interface for Firestore operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { QueryConstraint } from 'firebase/firestore'
|
|
7
|
+
import type { User } from '../entities'
|
|
8
|
+
|
|
9
|
+
export interface IFirestoreService {
|
|
10
|
+
/**
|
|
11
|
+
* Get user by ID
|
|
12
|
+
*/
|
|
13
|
+
getUser(userId: string): Promise<User | null>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get user by email
|
|
17
|
+
*/
|
|
18
|
+
getUserByEmail(email: string): Promise<User | null>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create user
|
|
22
|
+
*/
|
|
23
|
+
createUser(userId: string, data: Partial<User>): Promise<void>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Update user
|
|
27
|
+
*/
|
|
28
|
+
updateUser(userId: string, data: Partial<User>): Promise<void>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Delete user
|
|
32
|
+
*/
|
|
33
|
+
deleteUser(userId: string): Promise<void>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Update user profile
|
|
37
|
+
*/
|
|
38
|
+
updateProfile(userId: string, updates: Partial<Pick<User['profile'], 'displayName' | 'photoURL' | 'phoneNumber'>>): Promise<void>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update user settings
|
|
42
|
+
*/
|
|
43
|
+
updateSettings(userId: string, settings: Partial<User['settings']>): Promise<void>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Update user subscription
|
|
47
|
+
*/
|
|
48
|
+
updateSubscription(userId: string, subscription: Partial<User['subscription']>): Promise<void>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update last login timestamp
|
|
52
|
+
*/
|
|
53
|
+
updateLastLogin(userId: string): Promise<void>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Query users with constraints
|
|
57
|
+
*/
|
|
58
|
+
queryUsers(constraints: QueryConstraint[]): Promise<User[]>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to user document changes
|
|
62
|
+
*/
|
|
63
|
+
subscribeToUser(userId: string, callback: (user: User | null) => void, onError?: (error: Error) => void): () => void
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Domain Entities
|
|
3
|
+
* @description File and storage-related entities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File Metadata Entity
|
|
8
|
+
* Contains metadata about uploaded files
|
|
9
|
+
*/
|
|
10
|
+
export interface FileMetadata {
|
|
11
|
+
readonly id: string
|
|
12
|
+
readonly name: string
|
|
13
|
+
readonly fullPath: string
|
|
14
|
+
readonly contentType: string
|
|
15
|
+
readonly size: number
|
|
16
|
+
readonly createdAt: number
|
|
17
|
+
readonly updatedAt: number
|
|
18
|
+
readonly userId: string
|
|
19
|
+
readonly type: FileType
|
|
20
|
+
category?: FileCategory
|
|
21
|
+
description?: string
|
|
22
|
+
tags?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* File Types
|
|
27
|
+
*/
|
|
28
|
+
export type FileType = 'image' | 'video' | 'audio' | 'document' | 'other'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* File Categories
|
|
32
|
+
*/
|
|
33
|
+
export type FileCategory = 'profile' | 'content' | 'document' | 'attachment' | 'backup'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upload Progress Entity
|
|
37
|
+
* Tracks upload progress for resumable uploads
|
|
38
|
+
*/
|
|
39
|
+
export interface UploadProgress {
|
|
40
|
+
bytesTransferred: number
|
|
41
|
+
totalBytes: number
|
|
42
|
+
progress: number // 0-100
|
|
43
|
+
state: UploadState
|
|
44
|
+
speed?: number // bytes per second
|
|
45
|
+
remaining?: number // seconds remaining
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Upload States
|
|
50
|
+
*/
|
|
51
|
+
export type UploadState =
|
|
52
|
+
| 'paused'
|
|
53
|
+
| 'running'
|
|
54
|
+
| 'success'
|
|
55
|
+
| 'canceled'
|
|
56
|
+
| 'error'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* File Upload Result Entity
|
|
60
|
+
* Result of a file upload operation
|
|
61
|
+
*/
|
|
62
|
+
export interface FileUploadResult {
|
|
63
|
+
readonly id: string
|
|
64
|
+
readonly name: string
|
|
65
|
+
readonly fullPath: string
|
|
66
|
+
readonly downloadURL: string
|
|
67
|
+
readonly contentType: string
|
|
68
|
+
readonly size: number
|
|
69
|
+
readonly createdAt: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Export as UploadResult for backward compatibility
|
|
73
|
+
export type UploadResult = FileUploadResult
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Upload Options
|
|
77
|
+
* Configuration for file uploads
|
|
78
|
+
*/
|
|
79
|
+
export interface UploadOptions {
|
|
80
|
+
onProgress?: (progress: UploadProgress) => void
|
|
81
|
+
metadata?: FileMetadata
|
|
82
|
+
customMetadata?: Record<string, string>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Storage Statistics
|
|
87
|
+
*/
|
|
88
|
+
export interface StorageStats {
|
|
89
|
+
totalFiles: number
|
|
90
|
+
totalSize: number
|
|
91
|
+
filesByType: Record<FileType, number>
|
|
92
|
+
filesByCategory: Record<FileCategory, number>
|
|
93
|
+
lastUploadAt?: number
|
|
94
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Service
|
|
3
|
+
* @description Firebase Storage implementation of IStorageService
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ref,
|
|
8
|
+
uploadBytes,
|
|
9
|
+
uploadBytesResumable,
|
|
10
|
+
getDownloadURL,
|
|
11
|
+
deleteObject,
|
|
12
|
+
listAll,
|
|
13
|
+
getMetadata,
|
|
14
|
+
} from 'firebase/storage'
|
|
15
|
+
import { getFirebaseStorage } from '../../../infrastructure/firebase/client'
|
|
16
|
+
import type { IStorageService } from '../types'
|
|
17
|
+
import type {
|
|
18
|
+
UploadResult,
|
|
19
|
+
UploadOptions,
|
|
20
|
+
} from '../entities'
|
|
21
|
+
|
|
22
|
+
class StorageService implements IStorageService {
|
|
23
|
+
private get storage() {
|
|
24
|
+
return getFirebaseStorage()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Upload Methods
|
|
28
|
+
|
|
29
|
+
async uploadFile(
|
|
30
|
+
userId: string,
|
|
31
|
+
path: string,
|
|
32
|
+
file: File | Blob,
|
|
33
|
+
options?: UploadOptions
|
|
34
|
+
): Promise<UploadResult> {
|
|
35
|
+
const storageRef = ref(this.storage, `users/${userId}/${path}`)
|
|
36
|
+
const uploadTask = uploadBytesResumable(storageRef, file)
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
uploadTask.on(
|
|
40
|
+
'state_changed',
|
|
41
|
+
(snapshot) => {
|
|
42
|
+
if (options?.onProgress) {
|
|
43
|
+
const progress = {
|
|
44
|
+
bytesTransferred: snapshot.bytesTransferred,
|
|
45
|
+
totalBytes: snapshot.totalBytes,
|
|
46
|
+
progress: (snapshot.bytesTransferred / snapshot.totalBytes) * 100,
|
|
47
|
+
state: 'running' as const,
|
|
48
|
+
}
|
|
49
|
+
options.onProgress(progress)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
() => reject(new Error('Upload failed')),
|
|
53
|
+
async () => {
|
|
54
|
+
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref)
|
|
55
|
+
const metadata = await getMetadata(uploadTask.snapshot.ref)
|
|
56
|
+
|
|
57
|
+
resolve({
|
|
58
|
+
id: uploadTask.snapshot.ref.name,
|
|
59
|
+
name: metadata.name || uploadTask.snapshot.ref.name,
|
|
60
|
+
fullPath: metadata.fullPath || uploadTask.snapshot.ref.fullPath,
|
|
61
|
+
downloadURL,
|
|
62
|
+
contentType: metadata.contentType || '',
|
|
63
|
+
size: metadata.size || 0,
|
|
64
|
+
createdAt: metadata.timeCreated ? new Date(metadata.timeCreated).getTime() : Date.now(),
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async uploadImage(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
72
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
73
|
+
return this.uploadFile(userId, `images/${name}`, file)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async uploadVideo(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
77
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
78
|
+
return this.uploadFile(userId, `videos/${name}`, file)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async uploadDocument(userId: string, file: File, filename?: string): Promise<UploadResult> {
|
|
82
|
+
const name = filename || `${Date.now()}_${file.name}`
|
|
83
|
+
return this.uploadFile(userId, `documents/${name}`, file)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async uploadProfilePicture(userId: string, file: File): Promise<UploadResult> {
|
|
87
|
+
const storageRef = ref(this.storage, `users/${userId}/profile/${Date.now()}_${file.name}`)
|
|
88
|
+
await uploadBytes(storageRef, file)
|
|
89
|
+
const downloadURL = await getDownloadURL(storageRef)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
id: storageRef.name,
|
|
93
|
+
name: file.name,
|
|
94
|
+
fullPath: storageRef.fullPath,
|
|
95
|
+
downloadURL,
|
|
96
|
+
contentType: file.type,
|
|
97
|
+
size: file.size,
|
|
98
|
+
createdAt: Date.now(),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Download Methods
|
|
103
|
+
|
|
104
|
+
async getDownloadURL(path: string): Promise<string> {
|
|
105
|
+
try {
|
|
106
|
+
const storageRef = ref(this.storage, path)
|
|
107
|
+
return await getDownloadURL(storageRef)
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error('File not found')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete Methods
|
|
114
|
+
|
|
115
|
+
async deleteFile(path: string): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
const storageRef = ref(this.storage, path)
|
|
118
|
+
await deleteObject(storageRef)
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error('File not found')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async deleteUserFiles(userId: string): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
const userRef = ref(this.storage, `users/${userId}`)
|
|
127
|
+
const result = await listAll(userRef)
|
|
128
|
+
|
|
129
|
+
// Delete all files in all prefixes
|
|
130
|
+
for (const prefix of result.prefixes) {
|
|
131
|
+
const prefixResult = await listAll(prefix)
|
|
132
|
+
await Promise.all(prefixResult.items.map((item) => deleteObject(item)))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Delete all files in root
|
|
136
|
+
await Promise.all(result.items.map((item) => deleteObject(item)))
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error('Failed to delete user files')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async deleteImage(userId: string, filename: string): Promise<void> {
|
|
143
|
+
await this.deleteFile(`users/${userId}/images/${filename}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async deleteVideo(userId: string, filename: string): Promise<void> {
|
|
147
|
+
await this.deleteFile(`users/${userId}/videos/${filename}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async deleteProfilePicture(userId: string, filename: string): Promise<void> {
|
|
151
|
+
await this.deleteFile(`users/${userId}/profile/${filename}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// List Methods
|
|
155
|
+
|
|
156
|
+
async listUserFiles(userId: string, path?: string): Promise<string[]> {
|
|
157
|
+
const userRef = ref(this.storage, path ? `users/${userId}/${path}` : `users/${userId}`)
|
|
158
|
+
const result = await listAll(userRef)
|
|
159
|
+
|
|
160
|
+
const urls = await Promise.all(result.items.map((item) => getDownloadURL(item)))
|
|
161
|
+
return urls
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async listUserImages(userId: string): Promise<string[]> {
|
|
165
|
+
return this.listUserFiles(userId, 'images')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async listUserVideos(userId: string): Promise<string[]> {
|
|
169
|
+
return this.listUserFiles(userId, 'videos')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validation
|
|
173
|
+
|
|
174
|
+
validateFile(file: File, options?: { maxSizeBytes?: number; maxSizeMB?: number; allowedTypes?: string[] }): boolean {
|
|
175
|
+
const maxSizeBytes = options?.maxSizeBytes || (options?.maxSizeMB ? options.maxSizeMB * 1024 * 1024 : 10 * 1024 * 1024)
|
|
176
|
+
|
|
177
|
+
if (file.size > maxSizeBytes) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (options?.allowedTypes && !options.allowedTypes.includes(file.type)) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isImageFile(file: File): boolean {
|
|
189
|
+
return file.type.startsWith('image/')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
isVideoFile(file: File): boolean {
|
|
193
|
+
return file.type.startsWith('video/')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
isDocumentFile(file: File): boolean {
|
|
197
|
+
const docTypes = [
|
|
198
|
+
'application/pdf',
|
|
199
|
+
'application/msword',
|
|
200
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
201
|
+
'application/vnd.ms-excel',
|
|
202
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
203
|
+
'text/plain',
|
|
204
|
+
]
|
|
205
|
+
return docTypes.includes(file.type)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Utility Methods
|
|
209
|
+
|
|
210
|
+
generateUniqueFilename(originalName: string): string {
|
|
211
|
+
const timestamp = Date.now()
|
|
212
|
+
const random = Math.random().toString(36).substring(2, 8)
|
|
213
|
+
const extension = this.getFileExtension(originalName)
|
|
214
|
+
return `${timestamp}_${random}.${extension}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getFileExtension(filename: string): string {
|
|
218
|
+
return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Export class and singleton instance
|
|
223
|
+
export const storageService = new StorageService()
|