@umituz/react-native-firebase 2.6.0 → 2.6.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/package.json +1 -1
- package/src/application/auth/index.ts +42 -0
- package/src/application/auth/ports/AuthPort.ts +164 -0
- package/src/application/auth/use-cases/SignInUseCase.ts +253 -0
- package/src/application/auth/use-cases/SignOutUseCase.ts +288 -0
- package/src/application/auth/use-cases/index.ts +26 -0
- package/src/domains/account-deletion/domain/index.ts +15 -0
- package/src/domains/account-deletion/domain/services/UserValidationService.ts +295 -0
- package/src/domains/account-deletion/index.ts +43 -6
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor.ts +230 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler.ts +174 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository.ts +266 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionTypes.ts +33 -0
- package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +39 -227
- package/src/domains/auth/domain.ts +16 -0
- package/src/domains/auth/index.ts +7 -148
- package/src/domains/auth/infrastructure.ts +156 -0
- package/src/domains/auth/presentation/hooks/GoogleOAuthHookService.ts +247 -0
- package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +49 -103
- package/src/domains/auth/presentation.ts +25 -0
- package/src/domains/firestore/domain/entities/Collection.ts +288 -0
- package/src/domains/firestore/domain/entities/Document.ts +233 -0
- package/src/domains/firestore/domain/index.ts +30 -0
- package/src/domains/firestore/domain/services/QueryService.ts +182 -0
- package/src/domains/firestore/domain/services/QueryServiceAnalysis.ts +169 -0
- package/src/domains/firestore/domain/services/QueryServiceHelpers.ts +151 -0
- package/src/domains/firestore/domain/value-objects/QueryOptions.ts +191 -0
- package/src/domains/firestore/domain/value-objects/QueryOptions.ts.bak +320 -0
- package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization.ts +207 -0
- package/src/domains/firestore/domain/value-objects/QueryOptionsValidation.ts +182 -0
- package/src/domains/firestore/domain/value-objects/WhereClause.ts +299 -0
- package/src/domains/firestore/domain/value-objects/WhereClauseFactory.ts +207 -0
- package/src/domains/firestore/index.ts +9 -6
- package/src/index.ts +25 -0
- package/src/shared/domain/utils/error-handlers/error-messages.ts +11 -0
- package/src/shared/infrastructure/base/ErrorHandler.ts +189 -0
- package/src/shared/infrastructure/base/ServiceBase.ts +220 -0
- package/src/shared/infrastructure/base/TypedGuard.ts +131 -0
- package/src/shared/infrastructure/base/index.ts +34 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign Out Use Case
|
|
3
|
+
* Single Responsibility: Handle user sign out operations
|
|
4
|
+
*
|
|
5
|
+
* Application use case for user sign out.
|
|
6
|
+
* Ensures proper cleanup and state management.
|
|
7
|
+
*
|
|
8
|
+
* Max lines: 150 (enforced for maintainability)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { User } from 'firebase/auth';
|
|
12
|
+
import type { Result } from '../../../shared/domain/utils';
|
|
13
|
+
import type { IAuthPort } from '../ports/AuthPort';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sign out use case result
|
|
17
|
+
*/
|
|
18
|
+
export interface SignOutUseCaseResult extends Result<void> {
|
|
19
|
+
readonly wasSignedIn: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sign out use case options
|
|
24
|
+
*/
|
|
25
|
+
export interface SignOutOptions {
|
|
26
|
+
/** Clear local data */
|
|
27
|
+
readonly clearLocalData?: boolean;
|
|
28
|
+
/** Navigate to sign in screen */
|
|
29
|
+
readonly navigateToSignIn?: boolean;
|
|
30
|
+
/** Show confirmation dialog */
|
|
31
|
+
readonly showConfirmation?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sign out use case
|
|
36
|
+
* Handles user sign out with proper cleanup
|
|
37
|
+
*/
|
|
38
|
+
export class SignOutUseCase {
|
|
39
|
+
private readonly authPort: IAuthPort;
|
|
40
|
+
|
|
41
|
+
constructor(authPort: IAuthPort) {
|
|
42
|
+
this.authPort = authPort;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute sign out use case
|
|
47
|
+
* Signs out user and performs cleanup
|
|
48
|
+
*/
|
|
49
|
+
async execute(options: SignOutOptions = {}): Promise<SignOutUseCaseResult> {
|
|
50
|
+
const wasSignedIn = this.authPort.isAuthenticated();
|
|
51
|
+
|
|
52
|
+
// Check if user is signed in
|
|
53
|
+
if (!wasSignedIn) {
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
wasSignedIn: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sign out from auth
|
|
61
|
+
const result = await this.authPort.signOut();
|
|
62
|
+
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: result.error,
|
|
67
|
+
wasSignedIn,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Perform cleanup
|
|
72
|
+
await this.performCleanup(options);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
wasSignedIn,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Perform cleanup after sign out
|
|
82
|
+
*/
|
|
83
|
+
private async performCleanup(options: SignOutOptions): Promise<void> {
|
|
84
|
+
// Clear local data if requested
|
|
85
|
+
if (options.clearLocalData) {
|
|
86
|
+
await this.clearLocalData();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Additional cleanup can be added here
|
|
90
|
+
// For example: clear cache, reset state, etc.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear local data
|
|
95
|
+
* Override in subclass for custom cleanup
|
|
96
|
+
*/
|
|
97
|
+
protected async clearLocalData(): Promise<void> {
|
|
98
|
+
// Default implementation does nothing
|
|
99
|
+
// Subclass can override for custom cleanup
|
|
100
|
+
if (__DEV__) {
|
|
101
|
+
console.log('[SignOutUseCase] Clearing local data');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if user is signed in
|
|
107
|
+
*/
|
|
108
|
+
isSignedIn(): boolean {
|
|
109
|
+
return this.authPort.isAuthenticated();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get current user
|
|
114
|
+
*/
|
|
115
|
+
getCurrentUser(): User | null {
|
|
116
|
+
return this.authPort.getCurrentUser();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get current user ID
|
|
121
|
+
*/
|
|
122
|
+
getCurrentUserId(): string | null {
|
|
123
|
+
return this.authPort.getCurrentUserId();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if user is anonymous
|
|
128
|
+
*/
|
|
129
|
+
isAnonymous(): boolean {
|
|
130
|
+
return this.authPort.isAnonymous();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Confirm sign out
|
|
135
|
+
* Useful for showing confirmation dialog
|
|
136
|
+
*/
|
|
137
|
+
async confirmSignOut(): Promise<boolean> {
|
|
138
|
+
// Default implementation always returns true
|
|
139
|
+
// Override in subclass to show confirmation dialog
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate can sign out
|
|
145
|
+
* Check if sign out is allowed
|
|
146
|
+
*/
|
|
147
|
+
async canSignOut(): Promise<Result<void>> {
|
|
148
|
+
if (!this.isSignedIn()) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: {
|
|
152
|
+
code: 'auth/not-signed-in',
|
|
153
|
+
message: 'No user is currently signed in',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Additional checks can be added here
|
|
159
|
+
// For example: check for unsaved changes, active operations, etc.
|
|
160
|
+
|
|
161
|
+
return { success: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sign out with confirmation
|
|
166
|
+
* Shows confirmation dialog before signing out
|
|
167
|
+
*/
|
|
168
|
+
async signOutWithConfirmation(): Promise<SignOutUseCaseResult> {
|
|
169
|
+
const confirmed = await this.confirmSignOut();
|
|
170
|
+
|
|
171
|
+
if (!confirmed) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: {
|
|
175
|
+
code: 'auth/sign-out-cancelled',
|
|
176
|
+
message: 'Sign out was cancelled',
|
|
177
|
+
},
|
|
178
|
+
wasSignedIn: this.isSignedIn(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this.execute();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Sign out and navigate
|
|
187
|
+
* Signs out and navigates to sign in screen
|
|
188
|
+
*/
|
|
189
|
+
async signOutAndNavigate(): Promise<SignOutUseCaseResult> {
|
|
190
|
+
return this.execute({
|
|
191
|
+
clearLocalData: true,
|
|
192
|
+
navigateToSignIn: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Force sign out
|
|
198
|
+
* Signs out even if there are errors
|
|
199
|
+
*/
|
|
200
|
+
async forceSignOut(): Promise<void> {
|
|
201
|
+
try {
|
|
202
|
+
await this.execute({
|
|
203
|
+
clearLocalData: true,
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Ignore errors during force sign out
|
|
207
|
+
if (__DEV__) {
|
|
208
|
+
console.error('[SignOutUseCase] Force sign out error:', error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ensure local data is cleared
|
|
213
|
+
await this.clearLocalData();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get sign out statistics
|
|
218
|
+
*/
|
|
219
|
+
getSignOutStats(): {
|
|
220
|
+
readonly isSignedIn: boolean;
|
|
221
|
+
readonly isAnonymous: boolean;
|
|
222
|
+
readonly hasUserId: boolean;
|
|
223
|
+
} {
|
|
224
|
+
return {
|
|
225
|
+
isSignedIn: this.isSignedIn(),
|
|
226
|
+
isAnonymous: this.isAnonymous(),
|
|
227
|
+
hasUserId: this.getCurrentUserId() !== null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create sign out options
|
|
233
|
+
*/
|
|
234
|
+
createOptions(options: Partial<SignOutOptions> = {}): SignOutOptions {
|
|
235
|
+
return {
|
|
236
|
+
clearLocalData: false,
|
|
237
|
+
navigateToSignIn: false,
|
|
238
|
+
showConfirmation: false,
|
|
239
|
+
...options,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Quick sign out helper
|
|
245
|
+
*/
|
|
246
|
+
async quickSignOut(): Promise<SignOutUseCaseResult> {
|
|
247
|
+
return this.execute();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if confirmation should be shown
|
|
252
|
+
*/
|
|
253
|
+
shouldShowConfirmation(): boolean {
|
|
254
|
+
// Default: show confirmation for authenticated users
|
|
255
|
+
return this.isSignedIn() && !this.isAnonymous();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get user-friendly sign out message
|
|
260
|
+
*/
|
|
261
|
+
getSignOutMessage(): string {
|
|
262
|
+
if (!this.isSignedIn()) {
|
|
263
|
+
return 'You are not signed in';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this.isAnonymous()) {
|
|
267
|
+
return 'Sign out from guest session?';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return 'Are you sure you want to sign out?';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Factory function to create sign out use case
|
|
276
|
+
*/
|
|
277
|
+
export function createSignOutUseCase(authPort: IAuthPort): SignOutUseCase {
|
|
278
|
+
return new SignOutUseCase(authPort);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Factory function to create sign out use case with default auth port
|
|
283
|
+
*/
|
|
284
|
+
export async function createDefaultSignOutUseCase(): Promise<SignOutUseCase> {
|
|
285
|
+
const { createAuthPort } = await import('../ports/AuthPort');
|
|
286
|
+
const authPort = createAuthPort();
|
|
287
|
+
return createSignOutUseCase(authPort);
|
|
288
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Use Cases
|
|
3
|
+
* Application layer use cases for authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
SignInUseCaseResult,
|
|
8
|
+
SignInOptions,
|
|
9
|
+
} from './SignInUseCase';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
SignInUseCase,
|
|
13
|
+
createSignInUseCase,
|
|
14
|
+
createDefaultSignInUseCase,
|
|
15
|
+
} from './SignInUseCase';
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
SignOutUseCaseResult,
|
|
19
|
+
SignOutOptions,
|
|
20
|
+
} from './SignOutUseCase';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
SignOutUseCase,
|
|
24
|
+
createSignOutUseCase,
|
|
25
|
+
createDefaultSignOutUseCase,
|
|
26
|
+
} from './SignOutUseCase';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Deletion Domain Layer
|
|
3
|
+
* Domain-Driven Design (DDD) - Domain Exports
|
|
4
|
+
*
|
|
5
|
+
* Pure domain logic without infrastructure concerns.
|
|
6
|
+
* Exports domain services for account deletion operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Domain Services
|
|
10
|
+
export {
|
|
11
|
+
UserValidationService,
|
|
12
|
+
createUserValidationService,
|
|
13
|
+
userValidationService,
|
|
14
|
+
} from './services/UserValidationService';
|
|
15
|
+
export type { UserValidationResult } from './services/UserValidationService';
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Validation Service
|
|
3
|
+
* Single Responsibility: Validate user state for account operations
|
|
4
|
+
*
|
|
5
|
+
* Domain service that encapsulates user validation logic.
|
|
6
|
+
* Moves business logic from infrastructure to domain layer.
|
|
7
|
+
*
|
|
8
|
+
* Max lines: 150 (enforced for maintainability)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Auth, User } from 'firebase/auth';
|
|
12
|
+
import type { Result } from '../../../../shared/domain/utils';
|
|
13
|
+
import { successResult, failureResultFrom } from '../../../../shared/domain/utils';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User validation result
|
|
17
|
+
*/
|
|
18
|
+
export interface UserValidationResult {
|
|
19
|
+
readonly valid: boolean;
|
|
20
|
+
readonly error?: { code: string; message: string };
|
|
21
|
+
readonly userId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* User validation service
|
|
26
|
+
* Provides user validation functionality for account operations
|
|
27
|
+
*/
|
|
28
|
+
export class UserValidationService {
|
|
29
|
+
/**
|
|
30
|
+
* Validate user is ready for deletion
|
|
31
|
+
* Checks user exists, not anonymous, and has required provider
|
|
32
|
+
*/
|
|
33
|
+
validateForDeletion(user: User | null): Result<{ userId: string; provider: string }> {
|
|
34
|
+
if (!user) {
|
|
35
|
+
return failureResultFrom('auth/not-ready', 'No authenticated user');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (user.isAnonymous) {
|
|
39
|
+
return failureResultFrom('auth/anonymous', 'Cannot delete anonymous account');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const provider = this.getUserAuthProvider(user);
|
|
43
|
+
if (!provider) {
|
|
44
|
+
return failureResultFrom('auth/unsupported', 'Unsupported auth provider');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return successResult({
|
|
48
|
+
userId: user.uid,
|
|
49
|
+
provider,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate user hasn't changed during operation
|
|
55
|
+
* Critical for preventing race conditions
|
|
56
|
+
*/
|
|
57
|
+
validateUserUnchanged(auth: Auth | null, originalUserId: string): Result<void> {
|
|
58
|
+
if (!auth) {
|
|
59
|
+
return failureResultFrom('auth/not-ready', 'Auth not initialized');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const currentUserId = auth.currentUser?.uid;
|
|
63
|
+
|
|
64
|
+
if (!currentUserId) {
|
|
65
|
+
return failureResultFrom('auth/user-signed-out', 'User signed out during operation');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (currentUserId !== originalUserId) {
|
|
69
|
+
return failureResultFrom('auth/user-changed', 'User changed during operation');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return successResult();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate user credentials are present
|
|
77
|
+
*/
|
|
78
|
+
validateCredentials(
|
|
79
|
+
user: User,
|
|
80
|
+
options: {
|
|
81
|
+
password?: string;
|
|
82
|
+
googleIdToken?: string;
|
|
83
|
+
}
|
|
84
|
+
): Result<void> {
|
|
85
|
+
const provider = this.getUserAuthProvider(user);
|
|
86
|
+
|
|
87
|
+
if (provider === 'password' && !options.password) {
|
|
88
|
+
return failureResultFrom('auth/password-required', 'Password required for reauthentication');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (provider === 'google.com' && !options.googleIdToken) {
|
|
92
|
+
return failureResultFrom('auth/google-token-required', 'Google ID token required for reauthentication');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return successResult();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get user's auth provider
|
|
100
|
+
* Returns provider ID or null if unknown
|
|
101
|
+
*/
|
|
102
|
+
getUserAuthProvider(user: User): string | null {
|
|
103
|
+
if (!user.providerData || user.providerData.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check all providers
|
|
108
|
+
for (const userInfo of user.providerData) {
|
|
109
|
+
if (userInfo.providerId) {
|
|
110
|
+
return userInfo.providerId;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if user has specific provider
|
|
119
|
+
*/
|
|
120
|
+
hasProvider(user: User, providerId: string): boolean {
|
|
121
|
+
if (!user.providerData || user.providerData.length === 0) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return user.providerData.some(
|
|
126
|
+
userInfo => userInfo.providerId === providerId
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if user is email/password user
|
|
132
|
+
*/
|
|
133
|
+
isEmailPasswordUser(user: User): boolean {
|
|
134
|
+
return this.hasProvider(user, 'password');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if user is Google user
|
|
139
|
+
*/
|
|
140
|
+
isGoogleUser(user: User): boolean {
|
|
141
|
+
return this.hasProvider(user, 'google.com');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if user is Apple user
|
|
146
|
+
*/
|
|
147
|
+
isAppleUser(user: User): boolean {
|
|
148
|
+
return this.hasProvider(user, 'apple.com');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if user requires reauthentication
|
|
153
|
+
* Based on provider and operation type
|
|
154
|
+
*/
|
|
155
|
+
requiresReauthentication(user: User, operation: 'delete' | 'update'): boolean {
|
|
156
|
+
// Email/password users always need recent auth
|
|
157
|
+
if (this.isEmailPasswordUser(user)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Social providers may require recent auth for sensitive operations
|
|
162
|
+
if (operation === 'delete') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate user email
|
|
171
|
+
*/
|
|
172
|
+
validateEmail(user: User): Result<void> {
|
|
173
|
+
const email = user.email;
|
|
174
|
+
|
|
175
|
+
if (!email) {
|
|
176
|
+
return failureResultFrom('auth/no-email', 'User has no email');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
180
|
+
if (!emailRegex.test(email)) {
|
|
181
|
+
return failureResultFrom('auth/invalid-email', 'Invalid email format');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return successResult();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validate user is verified (if applicable)
|
|
189
|
+
*/
|
|
190
|
+
validateVerified(user: User, requireVerification: boolean = false): Result<void> {
|
|
191
|
+
if (requireVerification && !user.emailVerified) {
|
|
192
|
+
return failureResultFrom('auth/unverified', 'Email not verified');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return successResult();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get user metadata
|
|
200
|
+
*/
|
|
201
|
+
getUserMetadata(user: User): {
|
|
202
|
+
readonly createdAt: number | null;
|
|
203
|
+
readonly lastSignInAt: number | null;
|
|
204
|
+
} {
|
|
205
|
+
return {
|
|
206
|
+
createdAt: user.metadata.creationTime ? new Date(user.metadata.creationTime).getTime() : null,
|
|
207
|
+
lastSignInAt: user.metadata.lastSignInTime ? new Date(user.metadata.lastSignInTime).getTime() : null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if account is new (created recently)
|
|
213
|
+
*/
|
|
214
|
+
isAccountNew(user: User, maxAgeMs: number = 24 * 60 * 60 * 1000): boolean {
|
|
215
|
+
const metadata = this.getUserMetadata(user);
|
|
216
|
+
if (!metadata.createdAt) return false;
|
|
217
|
+
|
|
218
|
+
const age = Date.now() - metadata.createdAt;
|
|
219
|
+
return age <= maxAgeMs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if user recently signed in
|
|
224
|
+
*/
|
|
225
|
+
isRecentSignIn(user: User, maxAgeMs: number = 5 * 60 * 1000): boolean {
|
|
226
|
+
const metadata = this.getUserMetadata(user);
|
|
227
|
+
if (!metadata.lastSignInAt) return false;
|
|
228
|
+
|
|
229
|
+
const timeSinceSignIn = Date.now() - metadata.lastSignInAt;
|
|
230
|
+
return timeSinceSignIn <= maxAgeMs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validate user can perform operation
|
|
235
|
+
* Comprehensive check combining multiple validations
|
|
236
|
+
*/
|
|
237
|
+
validateCanPerformOperation(
|
|
238
|
+
user: User | null,
|
|
239
|
+
operation: 'delete' | 'update',
|
|
240
|
+
options: {
|
|
241
|
+
requireVerified?: boolean;
|
|
242
|
+
maxSignInAge?: number;
|
|
243
|
+
password?: string;
|
|
244
|
+
googleIdToken?: string;
|
|
245
|
+
} = {}
|
|
246
|
+
): Result<{ userId: string; provider: string }> {
|
|
247
|
+
// Validate user ready for operation
|
|
248
|
+
const deletionValidation = this.validateForDeletion(user);
|
|
249
|
+
if (!deletionValidation.success) {
|
|
250
|
+
return deletionValidation;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const userId = deletionValidation.data!.userId;
|
|
254
|
+
const provider = deletionValidation.data!.provider;
|
|
255
|
+
|
|
256
|
+
// Validate email
|
|
257
|
+
if (user) {
|
|
258
|
+
const emailValidation = this.validateEmail(user);
|
|
259
|
+
if (!emailValidation.success) {
|
|
260
|
+
return emailValidation;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Validate verification status
|
|
264
|
+
const verifiedValidation = this.validateVerified(user, options.requireVerified);
|
|
265
|
+
if (!verifiedValidation.success) {
|
|
266
|
+
return verifiedValidation;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if recent sign-in required
|
|
270
|
+
if (options.maxSignInAge && !this.isRecentSignIn(user, options.maxSignInAge)) {
|
|
271
|
+
return failureResultFrom('auth/stale-session', 'Session too old, please sign in again');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Validate credentials
|
|
275
|
+
const credentialsValidation = this.validateCredentials(user, options);
|
|
276
|
+
if (!credentialsValidation.success) {
|
|
277
|
+
return credentialsValidation;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return successResult({ userId, provider });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Factory function to create user validation service
|
|
287
|
+
*/
|
|
288
|
+
export function createUserValidationService(): UserValidationService {
|
|
289
|
+
return new UserValidationService();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Default instance for convenience
|
|
294
|
+
*/
|
|
295
|
+
export const userValidationService = createUserValidationService();
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Account Deletion Domain
|
|
3
3
|
* Handles Firebase account deletion with reauthentication
|
|
4
|
+
*
|
|
5
|
+
* Domain-Driven Design (DDD) Architecture
|
|
4
6
|
*/
|
|
5
7
|
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// DOMAIN LAYER - Business Logic
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
6
12
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
UserValidationService,
|
|
14
|
+
createUserValidationService,
|
|
15
|
+
userValidationService,
|
|
16
|
+
} from './domain';
|
|
17
|
+
export type { UserValidationResult } from './domain';
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// APPLICATION LAYER - Use Cases & Ports
|
|
21
|
+
// =============================================================================
|
|
14
22
|
|
|
15
23
|
export type {
|
|
16
24
|
AccountDeletionOptions,
|
|
@@ -19,6 +27,35 @@ export type {
|
|
|
19
27
|
ReauthCredentialResult,
|
|
20
28
|
} from './application/ports/reauthentication.types';
|
|
21
29
|
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// INFRASTRUCTURE LAYER - Implementation
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
// Main Service (Refactored)
|
|
35
|
+
export {
|
|
36
|
+
deleteCurrentUser,
|
|
37
|
+
deleteUserAccount,
|
|
38
|
+
isDeletionInProgress,
|
|
39
|
+
} from './infrastructure/services/account-deletion.service';
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
AccountDeletionResult,
|
|
43
|
+
} from './infrastructure/services/account-deletion.service';
|
|
44
|
+
|
|
45
|
+
// Deletion Components
|
|
46
|
+
export {
|
|
47
|
+
AccountDeletionRepository,
|
|
48
|
+
createAccountDeletionRepository,
|
|
49
|
+
accountDeletionRepository,
|
|
50
|
+
} from './infrastructure/services/AccountDeletionRepository';
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
AccountDeletionExecutor,
|
|
54
|
+
createAccountDeletionExecutor,
|
|
55
|
+
accountDeletionExecutor,
|
|
56
|
+
} from './infrastructure/services/AccountDeletionExecutor';
|
|
57
|
+
|
|
58
|
+
// Reauthentication Service
|
|
22
59
|
export {
|
|
23
60
|
getUserAuthProvider,
|
|
24
61
|
reauthenticateWithPassword,
|