@umituz/react-native-auth 1.7.0 → 1.8.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 +1 -1
- package/src/application/ports/IAuthProvider.ts +52 -0
- package/src/application/ports/IAuthService.ts +6 -6
- package/src/domain/entities/AuthUser.ts +13 -0
- package/src/domain/value-objects/AuthConfig.ts +17 -14
- package/src/index.ts +45 -5
- package/src/infrastructure/providers/FirebaseAuthProvider.ts +131 -0
- package/src/infrastructure/services/AuthService.ts +113 -199
- package/src/infrastructure/storage/GuestModeStorage.ts +3 -2
- package/src/infrastructure/utils/AuthErrorMapper.ts +78 -34
- package/src/infrastructure/utils/AuthValidation.ts +125 -21
- package/src/presentation/hooks/useAuth.ts +5 -5
- package/src/presentation/hooks/useAuthState.ts +17 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design supports Firebase Auth and can be adapted for Supabase or other providers.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Provider Interface
|
|
3
|
+
* Port for provider-agnostic authentication (Firebase, Supabase, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
7
|
+
|
|
8
|
+
export interface AuthCredentials {
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SignUpCredentials extends AuthCredentials {
|
|
14
|
+
displayName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IAuthProvider {
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the auth provider
|
|
20
|
+
*/
|
|
21
|
+
initialize(): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if provider is initialized
|
|
25
|
+
*/
|
|
26
|
+
isInitialized(): boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sign in with email and password
|
|
30
|
+
*/
|
|
31
|
+
signIn(credentials: AuthCredentials): Promise<AuthUser>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sign up with email, password, and optional display name
|
|
35
|
+
*/
|
|
36
|
+
signUp(credentials: SignUpCredentials): Promise<AuthUser>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sign out current user
|
|
40
|
+
*/
|
|
41
|
+
signOut(): Promise<void>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get current authenticated user
|
|
45
|
+
*/
|
|
46
|
+
getCurrentUser(): AuthUser | null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to auth state changes
|
|
50
|
+
*/
|
|
51
|
+
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void;
|
|
52
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Service Interface
|
|
3
|
-
* Port for authentication operations
|
|
3
|
+
* Port for authentication operations (provider-agnostic)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
7
7
|
|
|
8
8
|
export interface SignUpParams {
|
|
9
9
|
email: string;
|
|
@@ -20,12 +20,12 @@ export interface IAuthService {
|
|
|
20
20
|
/**
|
|
21
21
|
* Sign up a new user
|
|
22
22
|
*/
|
|
23
|
-
signUp(params: SignUpParams): Promise<
|
|
23
|
+
signUp(params: SignUpParams): Promise<AuthUser>;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Sign in an existing user
|
|
27
27
|
*/
|
|
28
|
-
signIn(params: SignInParams): Promise<
|
|
28
|
+
signIn(params: SignInParams): Promise<AuthUser>;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Sign out current user
|
|
@@ -40,12 +40,12 @@ export interface IAuthService {
|
|
|
40
40
|
/**
|
|
41
41
|
* Get current authenticated user
|
|
42
42
|
*/
|
|
43
|
-
getCurrentUser():
|
|
43
|
+
getCurrentUser(): AuthUser | null;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Subscribe to auth state changes
|
|
47
47
|
*/
|
|
48
|
-
onAuthStateChange(callback: (user:
|
|
48
|
+
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Check if auth is initialized
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth User Entity
|
|
3
|
+
* Provider-agnostic user representation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AuthUser {
|
|
7
|
+
uid: string;
|
|
8
|
+
email: string | null;
|
|
9
|
+
displayName: string | null;
|
|
10
|
+
isAnonymous: boolean;
|
|
11
|
+
emailVerified: boolean;
|
|
12
|
+
photoURL: string | null;
|
|
13
|
+
}
|
|
@@ -3,24 +3,27 @@
|
|
|
3
3
|
* Validates and stores authentication configuration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export interface PasswordConfig {
|
|
7
|
+
minLength: number;
|
|
8
|
+
requireUppercase: boolean;
|
|
9
|
+
requireLowercase: boolean;
|
|
10
|
+
requireNumber: boolean;
|
|
11
|
+
requireSpecialChar: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export interface AuthConfig {
|
|
7
|
-
|
|
8
|
-
minPasswordLength?: number;
|
|
9
|
-
/** Require uppercase letters in password */
|
|
10
|
-
requireUppercase?: boolean;
|
|
11
|
-
/** Require lowercase letters in password */
|
|
12
|
-
requireLowercase?: boolean;
|
|
13
|
-
/** Require numbers in password */
|
|
14
|
-
requireNumbers?: boolean;
|
|
15
|
-
/** Require special characters in password */
|
|
16
|
-
requireSpecialChars?: boolean;
|
|
15
|
+
password: PasswordConfig;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
18
|
+
export const DEFAULT_PASSWORD_CONFIG: PasswordConfig = {
|
|
19
|
+
minLength: 6,
|
|
21
20
|
requireUppercase: false,
|
|
22
21
|
requireLowercase: false,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
requireNumber: false,
|
|
23
|
+
requireSpecialChar: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
|
27
|
+
password: DEFAULT_PASSWORD_CONFIG,
|
|
25
28
|
};
|
|
26
29
|
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Architecture:
|
|
10
10
|
* - domain: Entities, value objects, errors (business logic)
|
|
11
11
|
* - application: Ports (interfaces)
|
|
12
|
-
* - infrastructure: Auth service implementation
|
|
12
|
+
* - infrastructure: Auth providers and service implementation
|
|
13
13
|
* - presentation: Hooks (React integration)
|
|
14
14
|
*
|
|
15
15
|
* Usage:
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
// =============================================================================
|
|
20
|
-
// DOMAIN LAYER -
|
|
20
|
+
// DOMAIN LAYER - Entities
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export type { AuthUser } from './domain/entities/AuthUser';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// DOMAIN LAYER - Errors
|
|
21
27
|
// =============================================================================
|
|
22
28
|
|
|
23
29
|
export {
|
|
@@ -33,17 +39,32 @@ export {
|
|
|
33
39
|
AuthInvalidEmailError,
|
|
34
40
|
} from './domain/errors/AuthError';
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// DOMAIN LAYER - Value Objects
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export type { AuthConfig, PasswordConfig } from './domain/value-objects/AuthConfig';
|
|
47
|
+
export { DEFAULT_AUTH_CONFIG, DEFAULT_PASSWORD_CONFIG } from './domain/value-objects/AuthConfig';
|
|
38
48
|
|
|
39
49
|
// =============================================================================
|
|
40
50
|
// APPLICATION LAYER - Ports
|
|
41
51
|
// =============================================================================
|
|
42
52
|
|
|
43
53
|
export type { IAuthService, SignUpParams, SignInParams } from './application/ports/IAuthService';
|
|
54
|
+
export type {
|
|
55
|
+
IAuthProvider,
|
|
56
|
+
AuthCredentials,
|
|
57
|
+
SignUpCredentials,
|
|
58
|
+
} from './application/ports/IAuthProvider';
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// INFRASTRUCTURE LAYER - Providers
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
export { FirebaseAuthProvider } from './infrastructure/providers/FirebaseAuthProvider';
|
|
44
65
|
|
|
45
66
|
// =============================================================================
|
|
46
|
-
// INFRASTRUCTURE LAYER -
|
|
67
|
+
// INFRASTRUCTURE LAYER - Services
|
|
47
68
|
// =============================================================================
|
|
48
69
|
|
|
49
70
|
export {
|
|
@@ -53,6 +74,24 @@ export {
|
|
|
53
74
|
resetAuthService,
|
|
54
75
|
} from './infrastructure/services/AuthService';
|
|
55
76
|
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// INFRASTRUCTURE LAYER - Validation
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
validateEmail,
|
|
83
|
+
validatePasswordForLogin,
|
|
84
|
+
validatePasswordForRegister,
|
|
85
|
+
validatePasswordConfirmation,
|
|
86
|
+
validateDisplayName,
|
|
87
|
+
} from './infrastructure/utils/AuthValidation';
|
|
88
|
+
|
|
89
|
+
export type {
|
|
90
|
+
ValidationResult,
|
|
91
|
+
PasswordStrengthResult,
|
|
92
|
+
PasswordRequirements,
|
|
93
|
+
} from './infrastructure/utils/AuthValidation';
|
|
94
|
+
|
|
56
95
|
// =============================================================================
|
|
57
96
|
// PRESENTATION LAYER - Hooks
|
|
58
97
|
// =============================================================================
|
|
@@ -72,6 +111,7 @@ export type {
|
|
|
72
111
|
AuthNavigatorProps,
|
|
73
112
|
} from './presentation/navigation/AuthNavigator';
|
|
74
113
|
|
|
114
|
+
// =============================================================================
|
|
75
115
|
// PRESENTATION LAYER - Components
|
|
76
116
|
// =============================================================================
|
|
77
117
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Auth Provider
|
|
3
|
+
* IAuthProvider implementation for Firebase Authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createUserWithEmailAndPassword,
|
|
8
|
+
signInWithEmailAndPassword,
|
|
9
|
+
signOut as firebaseSignOut,
|
|
10
|
+
onAuthStateChanged,
|
|
11
|
+
updateProfile,
|
|
12
|
+
type User,
|
|
13
|
+
type Auth,
|
|
14
|
+
} from "firebase/auth";
|
|
15
|
+
import type {
|
|
16
|
+
IAuthProvider,
|
|
17
|
+
AuthCredentials,
|
|
18
|
+
SignUpCredentials,
|
|
19
|
+
} from "../../application/ports/IAuthProvider";
|
|
20
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
21
|
+
import { mapFirebaseAuthError } from "../utils/AuthErrorMapper";
|
|
22
|
+
|
|
23
|
+
export class FirebaseAuthProvider implements IAuthProvider {
|
|
24
|
+
private auth: Auth | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(auth?: Auth) {
|
|
27
|
+
if (auth) {
|
|
28
|
+
this.auth = auth;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async initialize(): Promise<void> {
|
|
33
|
+
if (!this.auth) {
|
|
34
|
+
throw new Error("Firebase Auth instance must be provided");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setAuth(auth: Auth): void {
|
|
39
|
+
this.auth = auth;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isInitialized(): boolean {
|
|
43
|
+
return this.auth !== null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async signIn(credentials: AuthCredentials): Promise<AuthUser> {
|
|
47
|
+
if (!this.auth) {
|
|
48
|
+
throw new Error("Firebase Auth is not initialized");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const userCredential = await signInWithEmailAndPassword(
|
|
53
|
+
this.auth,
|
|
54
|
+
credentials.email.trim(),
|
|
55
|
+
credentials.password
|
|
56
|
+
);
|
|
57
|
+
return this.mapFirebaseUser(userCredential.user);
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
throw mapFirebaseAuthError(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async signUp(credentials: SignUpCredentials): Promise<AuthUser> {
|
|
64
|
+
if (!this.auth) {
|
|
65
|
+
throw new Error("Firebase Auth is not initialized");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const userCredential = await createUserWithEmailAndPassword(
|
|
70
|
+
this.auth,
|
|
71
|
+
credentials.email.trim(),
|
|
72
|
+
credentials.password
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (credentials.displayName && userCredential.user) {
|
|
76
|
+
try {
|
|
77
|
+
await updateProfile(userCredential.user, {
|
|
78
|
+
displayName: credentials.displayName.trim(),
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
// Don't fail signup if display name update fails
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.mapFirebaseUser(userCredential.user);
|
|
86
|
+
} catch (error: unknown) {
|
|
87
|
+
throw mapFirebaseAuthError(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async signOut(): Promise<void> {
|
|
92
|
+
if (!this.auth) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await firebaseSignOut(this.auth);
|
|
98
|
+
} catch (error: unknown) {
|
|
99
|
+
throw mapFirebaseAuthError(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getCurrentUser(): AuthUser | null {
|
|
104
|
+
if (!this.auth?.currentUser) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return this.mapFirebaseUser(this.auth.currentUser);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void {
|
|
111
|
+
if (!this.auth) {
|
|
112
|
+
callback(null);
|
|
113
|
+
return () => {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return onAuthStateChanged(this.auth, (user) => {
|
|
117
|
+
callback(user ? this.mapFirebaseUser(user) : null);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private mapFirebaseUser(user: User): AuthUser {
|
|
122
|
+
return {
|
|
123
|
+
uid: user.uid,
|
|
124
|
+
email: user.email,
|
|
125
|
+
displayName: user.displayName,
|
|
126
|
+
isAnonymous: user.isAnonymous,
|
|
127
|
+
emailVerified: user.emailVerified,
|
|
128
|
+
photoURL: user.photoURL,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,213 +1,159 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth Service
|
|
3
|
-
*
|
|
2
|
+
* Auth Service
|
|
3
|
+
* Provider-agnostic authentication orchestrator
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
createUserWithEmailAndPassword,
|
|
8
|
-
signInWithEmailAndPassword,
|
|
9
|
-
signOut as firebaseSignOut,
|
|
10
|
-
onAuthStateChanged,
|
|
11
|
-
updateProfile,
|
|
12
|
-
type User,
|
|
13
|
-
type Auth,
|
|
14
|
-
} from "firebase/auth";
|
|
6
|
+
import type { Auth } from "firebase/auth";
|
|
15
7
|
import type { IAuthService, SignUpParams, SignInParams } from "../../application/ports/IAuthService";
|
|
8
|
+
import type { IAuthProvider } from "../../application/ports/IAuthProvider";
|
|
9
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
10
|
+
import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
|
|
11
|
+
import { DEFAULT_AUTH_CONFIG } from "../../domain/value-objects/AuthConfig";
|
|
16
12
|
import {
|
|
17
13
|
AuthInitializationError,
|
|
18
14
|
AuthValidationError,
|
|
19
15
|
AuthWeakPasswordError,
|
|
20
16
|
AuthInvalidEmailError,
|
|
21
17
|
} from "../../domain/errors/AuthError";
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
import {
|
|
19
|
+
validateEmail,
|
|
20
|
+
validatePasswordForLogin,
|
|
21
|
+
validatePasswordForRegister,
|
|
22
|
+
validateDisplayName,
|
|
23
|
+
} from "../utils/AuthValidation";
|
|
24
|
+
import { FirebaseAuthProvider } from "../providers/FirebaseAuthProvider";
|
|
26
25
|
import { emitUserAuthenticated, emitGuestModeEnabled } from "../utils/AuthEventEmitter";
|
|
27
26
|
import { loadGuestMode, saveGuestMode, clearGuestMode } from "../storage/GuestModeStorage";
|
|
28
27
|
|
|
29
28
|
export class AuthService implements IAuthService {
|
|
30
|
-
private
|
|
29
|
+
private provider: IAuthProvider | null = null;
|
|
31
30
|
private config: AuthConfig;
|
|
32
31
|
private isGuestMode: boolean = false;
|
|
33
32
|
|
|
34
|
-
constructor(config: AuthConfig = {}) {
|
|
35
|
-
this.config = {
|
|
33
|
+
constructor(config: Partial<AuthConfig> = {}) {
|
|
34
|
+
this.config = {
|
|
35
|
+
...DEFAULT_AUTH_CONFIG,
|
|
36
|
+
...config,
|
|
37
|
+
password: {
|
|
38
|
+
...DEFAULT_AUTH_CONFIG.password,
|
|
39
|
+
...config.password,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
async initialize(auth: Auth): Promise<void> {
|
|
43
|
-
if (!auth) {
|
|
44
|
-
throw new AuthInitializationError("Auth instance is required");
|
|
44
|
+
async initialize(providerOrAuth: IAuthProvider | Auth): Promise<void> {
|
|
45
|
+
if (!providerOrAuth) {
|
|
46
|
+
throw new AuthInitializationError("Auth provider or Firebase Auth instance is required");
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
|
|
49
|
+
// Check if it's a Firebase Auth instance (has currentUser property)
|
|
50
|
+
if ("currentUser" in providerOrAuth) {
|
|
51
|
+
const firebaseProvider = new FirebaseAuthProvider(providerOrAuth as Auth);
|
|
52
|
+
await firebaseProvider.initialize();
|
|
53
|
+
this.provider = firebaseProvider;
|
|
54
|
+
} else {
|
|
55
|
+
this.provider = providerOrAuth as IAuthProvider;
|
|
56
|
+
await this.provider.initialize();
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
this.isGuestMode = await loadGuestMode();
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
/**
|
|
51
|
-
* Check if auth is initialized
|
|
52
|
-
*/
|
|
53
62
|
isInitialized(): boolean {
|
|
54
|
-
return this.
|
|
63
|
+
return this.provider !== null && this.provider.isInitialized();
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
private
|
|
58
|
-
if (!this.
|
|
59
|
-
|
|
60
|
-
if (__DEV__) {
|
|
61
|
-
console.warn("Auth service is not initialized. Call initialize() first.");
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
66
|
+
private getProvider(): IAuthProvider {
|
|
67
|
+
if (!this.provider || !this.provider.isInitialized()) {
|
|
68
|
+
throw new AuthInitializationError("Auth service is not initialized");
|
|
64
69
|
}
|
|
65
|
-
return this.
|
|
70
|
+
return this.provider;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*/
|
|
71
|
-
async signUp(params: SignUpParams): Promise<User> {
|
|
72
|
-
const auth = this.getAuth();
|
|
73
|
-
if (!auth) {
|
|
74
|
-
throw new AuthInitializationError("Auth service is not initialized");
|
|
75
|
-
}
|
|
73
|
+
async signUp(params: SignUpParams): Promise<AuthUser> {
|
|
74
|
+
const provider = this.getProvider();
|
|
76
75
|
|
|
77
76
|
// Validate email
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Validate password
|
|
83
|
-
const passwordValidation = validatePassword(params.password, this.config as any);
|
|
84
|
-
if (!passwordValidation.valid) {
|
|
85
|
-
throw new AuthWeakPasswordError(passwordValidation.error);
|
|
77
|
+
const emailResult = validateEmail(params.email);
|
|
78
|
+
if (!emailResult.isValid) {
|
|
79
|
+
throw new AuthInvalidEmailError(emailResult.error);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
params.password
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// Update display name if provided
|
|
97
|
-
if (params.displayName && userCredential.user) {
|
|
98
|
-
try {
|
|
99
|
-
await updateProfile(userCredential.user, {
|
|
100
|
-
displayName: params.displayName.trim(),
|
|
101
|
-
});
|
|
102
|
-
} catch (updateError) {
|
|
103
|
-
// Don't fail signup if display name update fails
|
|
104
|
-
// User can still use the app
|
|
105
|
-
}
|
|
82
|
+
// Validate display name if provided
|
|
83
|
+
if (params.displayName) {
|
|
84
|
+
const nameResult = validateDisplayName(params.displayName);
|
|
85
|
+
if (!nameResult.isValid) {
|
|
86
|
+
throw new AuthValidationError(nameResult.error || "Invalid name", "displayName");
|
|
106
87
|
}
|
|
88
|
+
}
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
// Validate password strength for registration
|
|
91
|
+
const passwordResult = validatePasswordForRegister(params.password, this.config.password);
|
|
92
|
+
if (!passwordResult.isValid) {
|
|
93
|
+
throw new AuthWeakPasswordError(passwordResult.error);
|
|
94
|
+
}
|
|
113
95
|
|
|
114
|
-
|
|
96
|
+
const user = await provider.signUp({
|
|
97
|
+
email: params.email,
|
|
98
|
+
password: params.password,
|
|
99
|
+
displayName: params.displayName,
|
|
100
|
+
});
|
|
115
101
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
102
|
+
// Clear guest mode when user signs up
|
|
103
|
+
if (this.isGuestMode) {
|
|
104
|
+
this.isGuestMode = false;
|
|
105
|
+
await clearGuestMode();
|
|
119
106
|
}
|
|
107
|
+
|
|
108
|
+
emitUserAuthenticated(user.uid);
|
|
109
|
+
return user;
|
|
120
110
|
}
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*/
|
|
125
|
-
async signIn(params: SignInParams): Promise<User> {
|
|
126
|
-
/* eslint-disable-next-line no-console */
|
|
127
|
-
if (__DEV__) {
|
|
128
|
-
console.log("[AuthService] signIn called with email:", params.email);
|
|
129
|
-
}
|
|
130
|
-
const auth = this.getAuth();
|
|
131
|
-
if (!auth) {
|
|
132
|
-
throw new AuthInitializationError("Auth service is not initialized");
|
|
133
|
-
}
|
|
112
|
+
async signIn(params: SignInParams): Promise<AuthUser> {
|
|
113
|
+
const provider = this.getProvider();
|
|
134
114
|
|
|
135
|
-
// Validate email
|
|
136
|
-
|
|
137
|
-
|
|
115
|
+
// Validate email format
|
|
116
|
+
const emailResult = validateEmail(params.email);
|
|
117
|
+
if (!emailResult.isValid) {
|
|
118
|
+
throw new AuthInvalidEmailError(emailResult.error);
|
|
138
119
|
}
|
|
139
120
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
121
|
+
// For login, only check if password is provided (no strength requirements)
|
|
122
|
+
const passwordResult = validatePasswordForLogin(params.password);
|
|
123
|
+
if (!passwordResult.isValid) {
|
|
124
|
+
throw new AuthValidationError(passwordResult.error || "Password is required", "password");
|
|
143
125
|
}
|
|
144
126
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const userCredential = await signInWithEmailAndPassword(
|
|
151
|
-
auth,
|
|
152
|
-
params.email.trim(),
|
|
153
|
-
params.password
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
/* eslint-disable-next-line no-console */
|
|
157
|
-
if (__DEV__) {
|
|
158
|
-
console.log("[AuthService] signInWithEmailAndPassword successful, user:", userCredential.user.uid);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Clear guest mode when user signs in
|
|
162
|
-
if (this.isGuestMode) {
|
|
163
|
-
this.isGuestMode = false;
|
|
164
|
-
await clearGuestMode();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
emitUserAuthenticated(userCredential.user.uid);
|
|
127
|
+
const user = await provider.signIn({
|
|
128
|
+
email: params.email,
|
|
129
|
+
password: params.password,
|
|
130
|
+
});
|
|
168
131
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.error("[AuthService] signIn error:", error);
|
|
174
|
-
}
|
|
175
|
-
throw mapFirebaseAuthError(error);
|
|
132
|
+
// Clear guest mode when user signs in
|
|
133
|
+
if (this.isGuestMode) {
|
|
134
|
+
this.isGuestMode = false;
|
|
135
|
+
await clearGuestMode();
|
|
176
136
|
}
|
|
137
|
+
|
|
138
|
+
emitUserAuthenticated(user.uid);
|
|
139
|
+
return user;
|
|
177
140
|
}
|
|
178
141
|
|
|
179
|
-
/**
|
|
180
|
-
* Sign out current user
|
|
181
|
-
*/
|
|
182
142
|
async signOut(): Promise<void> {
|
|
183
|
-
|
|
184
|
-
if (!auth) {
|
|
143
|
+
if (!this.provider) {
|
|
185
144
|
this.isGuestMode = false;
|
|
186
145
|
await clearGuestMode();
|
|
187
146
|
return;
|
|
188
147
|
}
|
|
189
148
|
|
|
190
|
-
|
|
191
|
-
await firebaseSignOut(auth);
|
|
192
|
-
} catch (error: any) {
|
|
193
|
-
throw mapFirebaseAuthError(error);
|
|
194
|
-
}
|
|
149
|
+
await this.provider.signOut();
|
|
195
150
|
}
|
|
196
151
|
|
|
197
|
-
/**
|
|
198
|
-
* Set guest mode (no authentication)
|
|
199
|
-
*/
|
|
200
152
|
async setGuestMode(): Promise<void> {
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
console.log("[AuthService] setGuestMode called");
|
|
204
|
-
}
|
|
205
|
-
const auth = this.getAuth();
|
|
206
|
-
|
|
207
|
-
// Sign out from Firebase if logged in
|
|
208
|
-
if (auth?.currentUser) {
|
|
153
|
+
// Sign out from provider if logged in
|
|
154
|
+
if (this.provider?.getCurrentUser()) {
|
|
209
155
|
try {
|
|
210
|
-
await
|
|
156
|
+
await this.provider.signOut();
|
|
211
157
|
} catch {
|
|
212
158
|
// Ignore sign out errors when switching to guest mode
|
|
213
159
|
}
|
|
@@ -218,35 +164,24 @@ export class AuthService implements IAuthService {
|
|
|
218
164
|
emitGuestModeEnabled();
|
|
219
165
|
}
|
|
220
166
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
*/
|
|
224
|
-
getCurrentUser(): User | null {
|
|
225
|
-
if (!this.auth) {
|
|
167
|
+
getCurrentUser(): AuthUser | null {
|
|
168
|
+
if (!this.provider) {
|
|
226
169
|
return null;
|
|
227
170
|
}
|
|
228
|
-
return this.
|
|
171
|
+
return this.provider.getCurrentUser();
|
|
229
172
|
}
|
|
230
173
|
|
|
231
|
-
/**
|
|
232
|
-
* Check if user is in guest mode
|
|
233
|
-
*/
|
|
234
174
|
getIsGuestMode(): boolean {
|
|
235
175
|
return this.isGuestMode;
|
|
236
176
|
}
|
|
237
177
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
*/
|
|
241
|
-
onAuthStateChange(callback: (user: User | null) => void): () => void {
|
|
242
|
-
const auth = this.getAuth();
|
|
243
|
-
if (!auth) {
|
|
244
|
-
// Return no-op unsubscribe if auth is not initialized
|
|
178
|
+
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void {
|
|
179
|
+
if (!this.provider) {
|
|
245
180
|
callback(null);
|
|
246
181
|
return () => {};
|
|
247
182
|
}
|
|
248
183
|
|
|
249
|
-
return
|
|
184
|
+
return this.provider.onAuthStateChange((user) => {
|
|
250
185
|
// Don't update if in guest mode
|
|
251
186
|
if (!this.isGuestMode) {
|
|
252
187
|
callback(user);
|
|
@@ -255,54 +190,34 @@ export class AuthService implements IAuthService {
|
|
|
255
190
|
}
|
|
256
191
|
});
|
|
257
192
|
}
|
|
193
|
+
|
|
194
|
+
getConfig(): AuthConfig {
|
|
195
|
+
return this.config;
|
|
196
|
+
}
|
|
258
197
|
}
|
|
259
198
|
|
|
260
|
-
|
|
261
|
-
* Singleton instance
|
|
262
|
-
* Apps should use initializeAuthService() to set up with their Firebase Auth instance
|
|
263
|
-
*/
|
|
199
|
+
// Singleton instance
|
|
264
200
|
let authServiceInstance: AuthService | null = null;
|
|
265
201
|
|
|
266
202
|
/**
|
|
267
|
-
* Initialize auth service with Firebase Auth instance
|
|
268
|
-
* Must be called before using any auth methods
|
|
269
|
-
*
|
|
270
|
-
* Uses DEFAULT_AUTH_CONFIG if no config is provided:
|
|
271
|
-
* - minPasswordLength: 6
|
|
272
|
-
* - requireUppercase: false
|
|
273
|
-
* - requireLowercase: false
|
|
274
|
-
* - requireNumbers: false
|
|
275
|
-
* - requireSpecialChars: false
|
|
276
|
-
*
|
|
277
|
-
* @param auth - Firebase Auth instance
|
|
278
|
-
* @param config - Optional auth configuration (defaults to permissive settings)
|
|
203
|
+
* Initialize auth service with provider or Firebase Auth instance
|
|
279
204
|
*/
|
|
280
205
|
export async function initializeAuthService(
|
|
281
|
-
|
|
282
|
-
config?: AuthConfig
|
|
206
|
+
providerOrAuth: IAuthProvider | Auth,
|
|
207
|
+
config?: Partial<AuthConfig>
|
|
283
208
|
): Promise<AuthService> {
|
|
284
|
-
// Use default config if not provided (permissive settings for better UX)
|
|
285
|
-
const finalConfig = config || DEFAULT_AUTH_CONFIG;
|
|
286
|
-
|
|
287
209
|
if (!authServiceInstance) {
|
|
288
|
-
authServiceInstance = new AuthService(
|
|
210
|
+
authServiceInstance = new AuthService(config);
|
|
289
211
|
}
|
|
290
|
-
await authServiceInstance.initialize(
|
|
212
|
+
await authServiceInstance.initialize(providerOrAuth);
|
|
291
213
|
return authServiceInstance;
|
|
292
214
|
}
|
|
293
215
|
|
|
294
216
|
/**
|
|
295
217
|
* Get auth service instance
|
|
296
|
-
* Returns null if service is not initialized (graceful degradation)
|
|
297
218
|
*/
|
|
298
219
|
export function getAuthService(): AuthService | null {
|
|
299
220
|
if (!authServiceInstance || !authServiceInstance.isInitialized()) {
|
|
300
|
-
/* eslint-disable-next-line no-console */
|
|
301
|
-
if (__DEV__) {
|
|
302
|
-
console.warn(
|
|
303
|
-
"Auth service is not initialized. Call initializeAuthService() first."
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
221
|
return null;
|
|
307
222
|
}
|
|
308
223
|
return authServiceInstance;
|
|
@@ -314,4 +229,3 @@ export function getAuthService(): AuthService | null {
|
|
|
314
229
|
export function resetAuthService(): void {
|
|
315
230
|
authServiceInstance = null;
|
|
316
231
|
}
|
|
317
|
-
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Single Responsibility: Manage guest mode persistence
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { storageRepository } from "@umituz/react-native-storage";
|
|
6
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
7
7
|
|
|
8
8
|
const GUEST_MODE_KEY = "@auth_guest_mode";
|
|
9
9
|
|
|
@@ -12,7 +12,8 @@ const GUEST_MODE_KEY = "@auth_guest_mode";
|
|
|
12
12
|
*/
|
|
13
13
|
export async function loadGuestMode(): Promise<boolean> {
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
15
|
+
const result = await storageRepository.getString(GUEST_MODE_KEY, "false");
|
|
16
|
+
const guestModeValue = unwrap(result, "false");
|
|
16
17
|
return guestModeValue === "true";
|
|
17
18
|
} catch {
|
|
18
19
|
return false;
|
|
@@ -17,41 +17,85 @@ import {
|
|
|
17
17
|
/**
|
|
18
18
|
* Map Firebase Auth errors to domain errors
|
|
19
19
|
*/
|
|
20
|
-
export function mapFirebaseAuthError(error:
|
|
21
|
-
const
|
|
22
|
-
const
|
|
20
|
+
export function mapFirebaseAuthError(error: unknown): Error {
|
|
21
|
+
const errorObj = error as { code?: string; message?: string } | undefined;
|
|
22
|
+
const code = errorObj?.code || "";
|
|
23
|
+
const message = errorObj?.message || "Authentication failed";
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (code === "auth/wrong-password") {
|
|
40
|
-
return new AuthWrongPasswordError();
|
|
41
|
-
}
|
|
42
|
-
if (code === "auth/network-request-failed") {
|
|
43
|
-
return new AuthNetworkError();
|
|
44
|
-
}
|
|
45
|
-
if (code === "auth/too-many-requests") {
|
|
46
|
-
return new AuthError("Too many requests. Please try again later.", "AUTH_TOO_MANY_REQUESTS");
|
|
47
|
-
}
|
|
48
|
-
if (code === "auth/configuration-not-found" || code === "auth/app-not-authorized") {
|
|
49
|
-
return new AuthConfigurationError("Authentication is not properly configured. Please contact support.");
|
|
50
|
-
}
|
|
51
|
-
if (code === "auth/operation-not-allowed") {
|
|
52
|
-
return new AuthConfigurationError("Email/password authentication is not enabled. Please contact support.");
|
|
53
|
-
}
|
|
25
|
+
switch (code) {
|
|
26
|
+
case "auth/email-already-in-use":
|
|
27
|
+
return new AuthEmailAlreadyInUseError();
|
|
28
|
+
|
|
29
|
+
case "auth/invalid-email":
|
|
30
|
+
return new AuthInvalidEmailError();
|
|
31
|
+
|
|
32
|
+
case "auth/weak-password":
|
|
33
|
+
return new AuthWeakPasswordError();
|
|
34
|
+
|
|
35
|
+
case "auth/user-disabled":
|
|
36
|
+
return new AuthError(
|
|
37
|
+
"Your account has been disabled. Please contact support.",
|
|
38
|
+
"AUTH_USER_DISABLED"
|
|
39
|
+
);
|
|
54
40
|
|
|
55
|
-
|
|
41
|
+
case "auth/user-not-found":
|
|
42
|
+
return new AuthUserNotFoundError();
|
|
43
|
+
|
|
44
|
+
case "auth/wrong-password":
|
|
45
|
+
return new AuthWrongPasswordError();
|
|
46
|
+
|
|
47
|
+
case "auth/invalid-credential":
|
|
48
|
+
return new AuthError(
|
|
49
|
+
"Invalid email or password. Please check your credentials.",
|
|
50
|
+
"AUTH_INVALID_CREDENTIAL"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
case "auth/invalid-login-credentials":
|
|
54
|
+
return new AuthError(
|
|
55
|
+
"Invalid email or password. Please check your credentials.",
|
|
56
|
+
"AUTH_INVALID_CREDENTIAL"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
case "auth/network-request-failed":
|
|
60
|
+
return new AuthNetworkError();
|
|
61
|
+
|
|
62
|
+
case "auth/too-many-requests":
|
|
63
|
+
return new AuthError(
|
|
64
|
+
"Too many failed attempts. Please wait a few minutes and try again.",
|
|
65
|
+
"AUTH_TOO_MANY_REQUESTS"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
case "auth/configuration-not-found":
|
|
69
|
+
case "auth/app-not-authorized":
|
|
70
|
+
return new AuthConfigurationError(
|
|
71
|
+
"Authentication is not properly configured. Please contact support."
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
case "auth/operation-not-allowed":
|
|
75
|
+
return new AuthConfigurationError(
|
|
76
|
+
"Email/password authentication is not enabled. Please contact support."
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
case "auth/requires-recent-login":
|
|
80
|
+
return new AuthError(
|
|
81
|
+
"Please sign in again to complete this action.",
|
|
82
|
+
"AUTH_REQUIRES_RECENT_LOGIN"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
case "auth/expired-action-code":
|
|
86
|
+
return new AuthError(
|
|
87
|
+
"This link has expired. Please request a new one.",
|
|
88
|
+
"AUTH_EXPIRED_ACTION_CODE"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
case "auth/invalid-action-code":
|
|
92
|
+
return new AuthError(
|
|
93
|
+
"This link is invalid. Please request a new one.",
|
|
94
|
+
"AUTH_INVALID_ACTION_CODE"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
return new AuthError(message, code || "AUTH_UNKNOWN_ERROR");
|
|
99
|
+
}
|
|
56
100
|
}
|
|
57
101
|
|
|
@@ -1,60 +1,164 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Validation Utilities
|
|
3
|
-
* Single Responsibility: Email and password validation
|
|
3
|
+
* Single Responsibility: Email and password validation for authentication
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { PasswordConfig } from "../../domain/value-objects/AuthConfig";
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PasswordStrengthResult extends ValidationResult {
|
|
14
|
+
requirements: PasswordRequirements;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PasswordRequirements {
|
|
18
|
+
hasMinLength: boolean;
|
|
19
|
+
hasUppercase: boolean;
|
|
20
|
+
hasLowercase: boolean;
|
|
21
|
+
hasNumber: boolean;
|
|
22
|
+
hasSpecialChar: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
26
|
+
const UPPERCASE_REGEX = /[A-Z]/;
|
|
27
|
+
const LOWERCASE_REGEX = /[a-z]/;
|
|
28
|
+
const NUMBER_REGEX = /[0-9]/;
|
|
29
|
+
const SPECIAL_CHAR_REGEX = /[!@#$%^&*(),.?":{}|<>]/;
|
|
7
30
|
|
|
8
31
|
/**
|
|
9
32
|
* Validate email format
|
|
10
33
|
*/
|
|
11
|
-
export function validateEmail(email: string):
|
|
12
|
-
|
|
13
|
-
|
|
34
|
+
export function validateEmail(email: string): ValidationResult {
|
|
35
|
+
if (!email || email.trim() === "") {
|
|
36
|
+
return { isValid: false, error: "Email is required" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!EMAIL_REGEX.test(email.trim())) {
|
|
40
|
+
return { isValid: false, error: "Please enter a valid email address" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { isValid: true };
|
|
14
44
|
}
|
|
15
45
|
|
|
16
46
|
/**
|
|
17
|
-
* Validate password
|
|
47
|
+
* Validate password for login - only checks if password is provided
|
|
48
|
+
* No strength requirements for login (existing users may have old passwords)
|
|
18
49
|
*/
|
|
19
|
-
export function
|
|
50
|
+
export function validatePasswordForLogin(password: string): ValidationResult {
|
|
51
|
+
if (!password || password.length === 0) {
|
|
52
|
+
return { isValid: false, error: "Password is required" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { isValid: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate password strength for registration
|
|
60
|
+
* Returns detailed requirements for UI feedback
|
|
61
|
+
*/
|
|
62
|
+
export function validatePasswordForRegister(
|
|
20
63
|
password: string,
|
|
21
|
-
config:
|
|
22
|
-
):
|
|
23
|
-
|
|
64
|
+
config: PasswordConfig
|
|
65
|
+
): PasswordStrengthResult {
|
|
66
|
+
const requirements: PasswordRequirements = {
|
|
67
|
+
hasMinLength: password.length >= config.minLength,
|
|
68
|
+
hasUppercase: !config.requireUppercase || UPPERCASE_REGEX.test(password),
|
|
69
|
+
hasLowercase: !config.requireLowercase || LOWERCASE_REGEX.test(password),
|
|
70
|
+
hasNumber: !config.requireNumber || NUMBER_REGEX.test(password),
|
|
71
|
+
hasSpecialChar:
|
|
72
|
+
!config.requireSpecialChar || SPECIAL_CHAR_REGEX.test(password),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!password || password.length === 0) {
|
|
24
76
|
return {
|
|
25
|
-
|
|
26
|
-
error:
|
|
77
|
+
isValid: false,
|
|
78
|
+
error: "Password is required",
|
|
79
|
+
requirements,
|
|
27
80
|
};
|
|
28
81
|
}
|
|
29
82
|
|
|
30
|
-
if (
|
|
83
|
+
if (!requirements.hasMinLength) {
|
|
31
84
|
return {
|
|
32
|
-
|
|
85
|
+
isValid: false,
|
|
86
|
+
error: `Password must be at least ${config.minLength} characters`,
|
|
87
|
+
requirements,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (config.requireUppercase && !UPPERCASE_REGEX.test(password)) {
|
|
92
|
+
return {
|
|
93
|
+
isValid: false,
|
|
33
94
|
error: "Password must contain at least one uppercase letter",
|
|
95
|
+
requirements,
|
|
34
96
|
};
|
|
35
97
|
}
|
|
36
98
|
|
|
37
|
-
if (config.requireLowercase &&
|
|
99
|
+
if (config.requireLowercase && !LOWERCASE_REGEX.test(password)) {
|
|
38
100
|
return {
|
|
39
|
-
|
|
101
|
+
isValid: false,
|
|
40
102
|
error: "Password must contain at least one lowercase letter",
|
|
103
|
+
requirements,
|
|
41
104
|
};
|
|
42
105
|
}
|
|
43
106
|
|
|
44
|
-
if (config.
|
|
107
|
+
if (config.requireNumber && !NUMBER_REGEX.test(password)) {
|
|
45
108
|
return {
|
|
46
|
-
|
|
109
|
+
isValid: false,
|
|
47
110
|
error: "Password must contain at least one number",
|
|
111
|
+
requirements,
|
|
48
112
|
};
|
|
49
113
|
}
|
|
50
114
|
|
|
51
|
-
if (config.
|
|
115
|
+
if (config.requireSpecialChar && !SPECIAL_CHAR_REGEX.test(password)) {
|
|
52
116
|
return {
|
|
53
|
-
|
|
117
|
+
isValid: false,
|
|
54
118
|
error: "Password must contain at least one special character",
|
|
119
|
+
requirements,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { isValid: true, requirements };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate password confirmation
|
|
128
|
+
*/
|
|
129
|
+
export function validatePasswordConfirmation(
|
|
130
|
+
password: string,
|
|
131
|
+
confirmPassword: string
|
|
132
|
+
): ValidationResult {
|
|
133
|
+
if (!confirmPassword) {
|
|
134
|
+
return { isValid: false, error: "Please confirm your password" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (password !== confirmPassword) {
|
|
138
|
+
return { isValid: false, error: "Passwords do not match" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { isValid: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate display name
|
|
146
|
+
*/
|
|
147
|
+
export function validateDisplayName(
|
|
148
|
+
displayName: string,
|
|
149
|
+
minLength: number = 2
|
|
150
|
+
): ValidationResult {
|
|
151
|
+
if (!displayName || displayName.trim() === "") {
|
|
152
|
+
return { isValid: false, error: "Name is required" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (displayName.trim().length < minLength) {
|
|
156
|
+
return {
|
|
157
|
+
isValid: false,
|
|
158
|
+
error: `Name must be at least ${minLength} characters`,
|
|
55
159
|
};
|
|
56
160
|
}
|
|
57
161
|
|
|
58
|
-
return {
|
|
162
|
+
return { isValid: true };
|
|
59
163
|
}
|
|
60
164
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAuth Hook
|
|
3
3
|
* React hook for authentication state management
|
|
4
|
-
*
|
|
5
|
-
* Uses
|
|
6
|
-
* Adds app-specific state (guest mode, error handling) on top of
|
|
4
|
+
*
|
|
5
|
+
* Uses provider-agnostic AuthUser type.
|
|
6
|
+
* Adds app-specific state (guest mode, error handling) on top of provider auth.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { User } from "firebase/auth";
|
|
10
9
|
import { useAuthState } from "./useAuthState";
|
|
11
10
|
import { useAuthActions } from "./useAuthActions";
|
|
11
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
12
12
|
|
|
13
13
|
export interface UseAuthResult {
|
|
14
14
|
/** Current authenticated user */
|
|
15
|
-
user:
|
|
15
|
+
user: AuthUser | null;
|
|
16
16
|
/** Whether auth state is loading */
|
|
17
17
|
loading: boolean;
|
|
18
18
|
/** Whether user is in guest mode */
|
|
@@ -4,13 +4,27 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useEffect } from "react";
|
|
7
|
-
import type { User } from "firebase/auth";
|
|
8
7
|
import { DeviceEventEmitter } from "react-native";
|
|
9
8
|
import { getAuthService } from "../../infrastructure/services/AuthService";
|
|
10
9
|
import { useFirebaseAuth } from "@umituz/react-native-firebase-auth";
|
|
10
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
11
|
+
|
|
12
|
+
type FirebaseUser = ReturnType<typeof useFirebaseAuth>["user"];
|
|
13
|
+
|
|
14
|
+
function mapToAuthUser(user: FirebaseUser): AuthUser | null {
|
|
15
|
+
if (!user) return null;
|
|
16
|
+
return {
|
|
17
|
+
uid: user.uid,
|
|
18
|
+
email: user.email,
|
|
19
|
+
displayName: user.displayName,
|
|
20
|
+
isAnonymous: user.isAnonymous,
|
|
21
|
+
emailVerified: user.emailVerified,
|
|
22
|
+
photoURL: user.photoURL,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
11
25
|
|
|
12
26
|
export interface UseAuthStateResult {
|
|
13
|
-
user:
|
|
27
|
+
user: AuthUser | null;
|
|
14
28
|
isAuthenticated: boolean;
|
|
15
29
|
isGuest: boolean;
|
|
16
30
|
loading: boolean;
|
|
@@ -32,7 +46,7 @@ export function useAuthState(): UseAuthStateResult {
|
|
|
32
46
|
const [error, setError] = useState<string | null>(null);
|
|
33
47
|
const [loading, setLoading] = useState(false);
|
|
34
48
|
|
|
35
|
-
const user = isGuest ? null : firebaseUser;
|
|
49
|
+
const user = isGuest ? null : mapToAuthUser(firebaseUser);
|
|
36
50
|
const isAuthenticated = !!user && !isGuest;
|
|
37
51
|
|
|
38
52
|
// Reset guest mode when user signs in
|