@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign In Use Case
|
|
3
|
+
* @description Handles user sign in with email/password
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { UserCredential } from 'firebase/auth'
|
|
7
|
+
import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
|
|
8
|
+
import type { SignInDTO } from '../../dto/auth.dto'
|
|
9
|
+
import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
|
|
10
|
+
|
|
11
|
+
export class SignInUseCase {
|
|
12
|
+
constructor(private readonly authRepository: IAuthRepository) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute sign in use case
|
|
16
|
+
*/
|
|
17
|
+
async execute(dto: SignInDTO): Promise<UserCredential> {
|
|
18
|
+
try {
|
|
19
|
+
// Validate input
|
|
20
|
+
this.validateDTO(dto)
|
|
21
|
+
|
|
22
|
+
// Perform sign in
|
|
23
|
+
const result = await this.authRepository.signIn(dto.email, dto.password)
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw this.handleError(error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate DTO
|
|
33
|
+
*/
|
|
34
|
+
private validateDTO(dto: SignInDTO): void {
|
|
35
|
+
if (!dto.email) {
|
|
36
|
+
throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Email is required')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!dto.password) {
|
|
40
|
+
throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Password is required')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Basic email validation
|
|
44
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
45
|
+
if (!emailRegex.test(dto.email)) {
|
|
46
|
+
throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid email format')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle errors
|
|
52
|
+
*/
|
|
53
|
+
private handleError(error: unknown): Error {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
// Firebase auth errors
|
|
56
|
+
const message = error.message.toLowerCase()
|
|
57
|
+
|
|
58
|
+
if (message.includes('user-not-found') || message.includes('invalid-email')) {
|
|
59
|
+
return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'User not found', error)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (message.includes('wrong-password') || message.includes('invalid-credential')) {
|
|
63
|
+
return createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials', error)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (message.includes('too-many-requests')) {
|
|
67
|
+
return createAuthError(AuthErrorCode.TOO_MANY_REQUESTS, 'Too many attempts', error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (message.includes('user-disabled')) {
|
|
71
|
+
return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'Account disabled', error)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return createAuthError(AuthErrorCode.SIGN_IN_FAILED, 'Sign in failed', error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign Out Use Case
|
|
3
|
+
* @description Handles user sign out
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
|
|
7
|
+
import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
|
|
8
|
+
|
|
9
|
+
export class SignOutUseCase {
|
|
10
|
+
constructor(private readonly authRepository: IAuthRepository) {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute sign out use case
|
|
14
|
+
*/
|
|
15
|
+
async execute(): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
await this.authRepository.signOut()
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw createAuthError(AuthErrorCode.SIGN_OUT_FAILED, 'Sign out failed', error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign Up Use Case
|
|
3
|
+
* @description Handles user registration with email/password
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
|
|
7
|
+
import type { IUserRepository } from '../../../domain/interfaces/user.repository.interface'
|
|
8
|
+
import type { SignUpDTO, SignUpResult } from '../../dto/auth.dto'
|
|
9
|
+
import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
|
|
10
|
+
|
|
11
|
+
export class SignUpUseCase {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly authRepository: IAuthRepository,
|
|
14
|
+
private readonly userRepository: IUserRepository
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute sign up use case
|
|
19
|
+
*/
|
|
20
|
+
async execute(dto: SignUpDTO): Promise<SignUpResult> {
|
|
21
|
+
try {
|
|
22
|
+
// Validate input
|
|
23
|
+
this.validateDTO(dto)
|
|
24
|
+
|
|
25
|
+
// Check if user already exists
|
|
26
|
+
const existingUser = await this.userRepository.getUserByEmail(dto.email)
|
|
27
|
+
if (existingUser) {
|
|
28
|
+
throw createAuthError(AuthErrorCode.USER_ALREADY_EXISTS, 'User already exists')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Perform sign up
|
|
32
|
+
const result = await this.authRepository.signUp(dto.email, dto.password, dto.displayName)
|
|
33
|
+
|
|
34
|
+
// Return enhanced result
|
|
35
|
+
return {
|
|
36
|
+
...result,
|
|
37
|
+
userId: result.user.uid,
|
|
38
|
+
emailVerified: result.user.emailVerified,
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw this.handleError(error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate DTO
|
|
47
|
+
*/
|
|
48
|
+
private validateDTO(dto: SignUpDTO): void {
|
|
49
|
+
if (!dto.email) {
|
|
50
|
+
throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Email is required')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!dto.password) {
|
|
54
|
+
throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Password is required')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!dto.displayName || dto.displayName.trim().length === 0) {
|
|
58
|
+
throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Display name is required')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Email validation
|
|
62
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
63
|
+
if (!emailRegex.test(dto.email)) {
|
|
64
|
+
throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Invalid email format')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Password strength validation
|
|
68
|
+
if (dto.password.length < 6) {
|
|
69
|
+
throw createAuthError(AuthErrorCode.WEAK_PASSWORD, 'Password must be at least 6 characters')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle errors
|
|
75
|
+
*/
|
|
76
|
+
private handleError(error: unknown): Error {
|
|
77
|
+
if (error instanceof Error && 'code' in error) {
|
|
78
|
+
const code = (error as { code: string }).code
|
|
79
|
+
|
|
80
|
+
if (code === 'auth/email-already-in-use') {
|
|
81
|
+
return createAuthError(AuthErrorCode.USER_ALREADY_EXISTS, 'Email already in use', error)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (code === 'auth/invalid-email') {
|
|
85
|
+
return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Invalid email', error)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (code === 'auth/weak-password') {
|
|
89
|
+
return createAuthError(AuthErrorCode.WEAK_PASSWORD, 'Password is too weak', error)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (code === 'auth/operation-not-allowed') {
|
|
93
|
+
return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Email/password accounts not enabled', error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Sign up failed', error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Use Cases
|
|
3
|
+
* @description Use cases for auth and user operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './auth/sign-in.use-case'
|
|
7
|
+
export * from './auth/sign-up.use-case'
|
|
8
|
+
export * from './auth/sign-in-with-google.use-case'
|
|
9
|
+
export * from './auth/reset-password.use-case'
|
|
10
|
+
export * from './auth/sign-out.use-case'
|
|
11
|
+
export * from './user/update-profile.use-case'
|
|
12
|
+
export * from './user/delete-account.use-case'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete Account Use Case
|
|
3
|
+
* @description Handles user account deletion
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
|
|
7
|
+
import type { DeleteAccountDTO } from '../../dto/auth.dto'
|
|
8
|
+
import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
|
|
9
|
+
|
|
10
|
+
export class DeleteAccountUseCase {
|
|
11
|
+
constructor(private readonly authRepository: IAuthRepository) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute account deletion use case
|
|
15
|
+
*/
|
|
16
|
+
async execute(dto: DeleteAccountDTO): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
// Validate input
|
|
19
|
+
this.validateDTO(dto)
|
|
20
|
+
|
|
21
|
+
// Check if user is authenticated
|
|
22
|
+
const currentUser = this.authRepository.getCurrentUser()
|
|
23
|
+
if (!currentUser) {
|
|
24
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!currentUser.email) {
|
|
28
|
+
throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'User email not available')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Delete account (includes password verification)
|
|
32
|
+
await this.authRepository.deleteAccount(dto.password)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw this.handleError(error)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate DTO
|
|
40
|
+
*/
|
|
41
|
+
private validateDTO(dto: DeleteAccountDTO): void {
|
|
42
|
+
if (!dto.password) {
|
|
43
|
+
throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Password is required')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (dto.password.length < 1) {
|
|
47
|
+
throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Password cannot be empty')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Handle errors
|
|
53
|
+
*/
|
|
54
|
+
private handleError(error: unknown): Error {
|
|
55
|
+
if (error instanceof Error && 'code' in error) {
|
|
56
|
+
const code = (error as { code: string }).code
|
|
57
|
+
|
|
58
|
+
if (code === 'auth/requires-recent-login') {
|
|
59
|
+
return createAuthError(AuthErrorCode.REAUTHENTICATION_REQUIRED, 'Please reauthenticate first', error)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (code === 'auth/wrong-password') {
|
|
63
|
+
return createAuthError(AuthErrorCode.REAUTHENTICATION_FAILED, 'Invalid password', error)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (code === 'auth/too-many-requests') {
|
|
67
|
+
return createAuthError(AuthErrorCode.TOO_MANY_REQUESTS, 'Too many requests', error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (code === 'auth/user-not-found') {
|
|
71
|
+
return createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'User not found', error)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Account deletion failed', error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Profile Use Case
|
|
3
|
+
* @description Handles user profile updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
|
|
7
|
+
import type { UpdateProfileDTO } from '../../dto/auth.dto'
|
|
8
|
+
import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
|
|
9
|
+
|
|
10
|
+
export class UpdateProfileUseCase {
|
|
11
|
+
constructor(private readonly authRepository: IAuthRepository) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute profile update use case
|
|
15
|
+
*/
|
|
16
|
+
async execute(dto: UpdateProfileDTO): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
// Validate input
|
|
19
|
+
this.validateDTO(dto)
|
|
20
|
+
|
|
21
|
+
// Check if user is authenticated
|
|
22
|
+
const currentUser = this.authRepository.getCurrentUser()
|
|
23
|
+
if (!currentUser) {
|
|
24
|
+
throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Update profile
|
|
28
|
+
await this.authRepository.updateProfile(dto)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw this.handleError(error)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate DTO
|
|
36
|
+
*/
|
|
37
|
+
private validateDTO(dto: UpdateProfileDTO): void {
|
|
38
|
+
if (!dto.displayName && !dto.photoURL) {
|
|
39
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'At least one field must be provided')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (dto.displayName !== undefined) {
|
|
43
|
+
if (typeof dto.displayName !== 'string') {
|
|
44
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name must be a string')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (dto.displayName.trim().length === 0) {
|
|
48
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name cannot be empty')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (dto.displayName.length > 100) {
|
|
52
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name too long (max 100 characters)')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (dto.photoURL !== undefined && dto.photoURL !== null) {
|
|
57
|
+
if (typeof dto.photoURL !== 'string') {
|
|
58
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Photo URL must be a string')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Basic URL validation
|
|
62
|
+
if (dto.photoURL && !this.isValidURL(dto.photoURL)) {
|
|
63
|
+
throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Invalid photo URL')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate URL format
|
|
70
|
+
*/
|
|
71
|
+
private isValidURL(url: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
new URL(url)
|
|
74
|
+
return true
|
|
75
|
+
} catch {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle errors
|
|
82
|
+
*/
|
|
83
|
+
private handleError(error: unknown): Error {
|
|
84
|
+
if (error instanceof Error && 'code' in error) {
|
|
85
|
+
const code = (error as { code: string }).code
|
|
86
|
+
|
|
87
|
+
if (code === 'auth/requires-recent-login') {
|
|
88
|
+
return createAuthError(AuthErrorCode.REAUTHENTICATION_REQUIRED, 'Please reauthenticate first', error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (code === 'auth/invalid-photo-url') {
|
|
92
|
+
return createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Invalid photo URL', error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Profile update failed', error)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File 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
|
+
* File Validation Options
|
|
87
|
+
*/
|
|
88
|
+
export interface FileValidationOptions {
|
|
89
|
+
maxSizeBytes?: number
|
|
90
|
+
maxSizeMB?: number
|
|
91
|
+
allowedTypes?: string[]
|
|
92
|
+
allowedExtensions?: string[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validation Result
|
|
97
|
+
*/
|
|
98
|
+
export interface ValidationResult {
|
|
99
|
+
valid: boolean
|
|
100
|
+
error?: string
|
|
101
|
+
errorCode?: FileErrorCode
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* File Error Codes
|
|
106
|
+
*/
|
|
107
|
+
export enum FileErrorCode {
|
|
108
|
+
FILE_TOO_LARGE = 'FILE_TOO_LARGE',
|
|
109
|
+
INVALID_TYPE = 'INVALID_TYPE',
|
|
110
|
+
INVALID_EXTENSION = 'INVALID_EXTENSION',
|
|
111
|
+
UPLOAD_FAILED = 'UPLOAD_FAILED',
|
|
112
|
+
DOWNLOAD_FAILED = 'DOWNLOAD_FAILED',
|
|
113
|
+
DELETE_FAILED = 'DELETE_FAILED',
|
|
114
|
+
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
|
|
115
|
+
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
116
|
+
CANCELLED = 'CANCELLED',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* File Filters
|
|
121
|
+
*/
|
|
122
|
+
export interface FileFilters {
|
|
123
|
+
type?: FileType
|
|
124
|
+
category?: FileCategory
|
|
125
|
+
startDate?: number
|
|
126
|
+
endDate?: number
|
|
127
|
+
minSize?: number
|
|
128
|
+
maxSize?: number
|
|
129
|
+
tags?: string[]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* File Query Result
|
|
134
|
+
*/
|
|
135
|
+
export interface FileQueryResult {
|
|
136
|
+
files: FileMetadata[]
|
|
137
|
+
totalCount: number
|
|
138
|
+
hasMore: boolean
|
|
139
|
+
lastDocId?: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Storage Statistics
|
|
144
|
+
*/
|
|
145
|
+
export interface StorageStats {
|
|
146
|
+
totalFiles: number
|
|
147
|
+
totalSize: number
|
|
148
|
+
filesByType: Record<FileType, number>
|
|
149
|
+
filesByCategory: Record<FileCategory, number>
|
|
150
|
+
lastUploadAt?: number
|
|
151
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp Value Objects
|
|
3
|
+
* @description Value objects for handling timestamps in Firestore
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Timestamp as FirestoreTimestamp } from 'firebase/firestore'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Firebase Timestamp wrapper
|
|
10
|
+
* Provides type safety and utility methods for timestamp handling
|
|
11
|
+
*/
|
|
12
|
+
export class Timestamp {
|
|
13
|
+
constructor(
|
|
14
|
+
public readonly seconds: number,
|
|
15
|
+
public readonly nanoseconds: number
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create Timestamp from JavaScript Date
|
|
20
|
+
*/
|
|
21
|
+
static fromDate(date: Date): Timestamp {
|
|
22
|
+
const milliseconds = date.getTime()
|
|
23
|
+
const seconds = Math.floor(milliseconds / 1000)
|
|
24
|
+
const nanoseconds = (milliseconds % 1000) * 1_000_000
|
|
25
|
+
return new Timestamp(seconds, nanoseconds)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create Timestamp from milliseconds
|
|
30
|
+
*/
|
|
31
|
+
static fromMillis(milliseconds: number): Timestamp {
|
|
32
|
+
const seconds = Math.floor(milliseconds / 1000)
|
|
33
|
+
const nanoseconds = (milliseconds % 1000) * 1_000_000
|
|
34
|
+
return new Timestamp(seconds, nanoseconds)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create Timestamp from Firestore Timestamp
|
|
39
|
+
*/
|
|
40
|
+
static fromFirestoreTimestamp(timestamp: FirestoreTimestamp): Timestamp {
|
|
41
|
+
return new Timestamp(timestamp.seconds, timestamp.nanoseconds)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get current timestamp
|
|
46
|
+
*/
|
|
47
|
+
static now(): Timestamp {
|
|
48
|
+
return Timestamp.fromDate(new Date())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert to JavaScript Date
|
|
53
|
+
*/
|
|
54
|
+
toDate(): Date {
|
|
55
|
+
return new Date(this.seconds * 1000 + this.nanoseconds / 1_000_000)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert to milliseconds
|
|
60
|
+
*/
|
|
61
|
+
toMillis(): number {
|
|
62
|
+
return this.seconds * 1000 + this.nanoseconds / 1_000_000
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert to Firestore Timestamp
|
|
67
|
+
*/
|
|
68
|
+
toFirestoreTimestamp(): FirestoreTimestamp {
|
|
69
|
+
return {
|
|
70
|
+
seconds: this.seconds,
|
|
71
|
+
nanoseconds: this.nanoseconds,
|
|
72
|
+
} as FirestoreTimestamp
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert to ISO string
|
|
77
|
+
*/
|
|
78
|
+
toISOString(): string {
|
|
79
|
+
return this.toDate().toISOString()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if timestamp is in the past
|
|
84
|
+
*/
|
|
85
|
+
isPast(): boolean {
|
|
86
|
+
return this.toMillis() < Date.now()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if timestamp is in the future
|
|
91
|
+
*/
|
|
92
|
+
isFuture(): boolean {
|
|
93
|
+
return this.toMillis() > Date.now()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get difference in milliseconds with another timestamp
|
|
98
|
+
*/
|
|
99
|
+
diff(other: Timestamp): number {
|
|
100
|
+
return this.toMillis() - other.toMillis()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Server timestamp sentinel for Firestore
|
|
106
|
+
*/
|
|
107
|
+
export const serverTimestamp = {
|
|
108
|
+
__type__: 'serverTimestamp',
|
|
109
|
+
} as const
|
|
110
|
+
|
|
111
|
+
export type ServerTimestamp = typeof serverTimestamp
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Timestamp field that can be a Timestamp, server timestamp, or ISO string
|
|
115
|
+
*/
|
|
116
|
+
export type TimestampField = Timestamp | ServerTimestamp | string
|