import { initializeApp, getApps, getApp, FirebaseApp } from 'firebase/app'; import { getAuth, Auth as FirebaseAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut as firebaseSignOut, onAuthStateChanged, User as FirebaseUser, GoogleAuthProvider, GithubAuthProvider, signInWithPopup, sendPasswordResetEmail, updateProfile, setPersistence, browserLocalPersistence, browserSessionPersistence, } from 'firebase/auth'; import { AuthConfig, AuthUser, SignInOptions, SignUpOptions, AuthProvider, AuthError } from '../types'; class AuthService { private app: FirebaseApp; private auth: FirebaseAuth; private config: AuthConfig; private customClaimsCache: Map = new Map(); constructor(config: AuthConfig) { this.config = config; // Initialize Firebase app this.app = getApps().length > 0 ? getApp() : initializeApp(config.firebaseConfig); this.auth = getAuth(this.app); } // Initialize auth with configuration async initialize(): Promise { // Set up auth state observer onAuthStateChanged(this.auth, async (user) => { if (user) { await this.enhanceUser(user); if (this.config.onUserCreated && this.isNewUser(user)) { await this.config.onUserCreated(user as AuthUser); } } }); } // Sign up with email and password async signUp(options: SignUpOptions): Promise<{ user: AuthUser }> { try { const { email, password, displayName, photoURL, metadata } = options; const userCredential = await createUserWithEmailAndPassword( this.auth, email, password ); // Update profile if display name or photo provided if (displayName || photoURL) { await updateProfile(userCredential.user, { displayName, photoURL, }); } // Store metadata in custom claims (server-side required) if (metadata) { await this.setUserMetadata(userCredential.user.uid, metadata); } const user = await this.enhanceUser(userCredential.user); return { user }; } catch (error: any) { throw this.normalizeError(error); } } // Sign in with email and password async signIn(options: SignInOptions): Promise<{ user: AuthUser }> { try { const { email, password, rememberMe = false } = options; // Set persistence based on remember me option await setPersistence( this.auth, rememberMe ? browserLocalPersistence : browserSessionPersistence ); const userCredential = await signInWithEmailAndPassword( this.auth, email, password ); const user = await this.enhanceUser(userCredential.user); return { user }; } catch (error: any) { throw this.normalizeError(error); } } // Sign in with social provider async signInWith(provider: AuthProvider): Promise<{ user: AuthUser }> { try { let authProvider; switch (provider) { case 'google': authProvider = new GoogleAuthProvider(); break; case 'github': authProvider = new GithubAuthProvider(); break; default: throw new Error(`Provider ${provider} not supported yet`); } const userCredential = await signInWithPopup(this.auth, authProvider); const user = await this.enhanceUser(userCredential.user); return { user }; } catch (error: any) { throw this.normalizeError(error); } } // Sign out async signOut(): Promise { try { await firebaseSignOut(this.auth); this.customClaimsCache.clear(); } catch (error: any) { throw this.normalizeError(error); } } // Send password reset email async sendPasswordResetEmail(email: string): Promise { try { await sendPasswordResetEmail(this.auth, email); } catch (error: any) { throw this.normalizeError(error); } } // Get current user getCurrentUser(): AuthUser | null { const user = this.auth.currentUser; if (!user) return null; // Return cached enhanced user return this.getCachedEnhancedUser(user); } // Subscribe to auth state changes onAuthStateChange(callback: (user: AuthUser | null) => void): () => void { return onAuthStateChanged(this.auth, async (user) => { if (user) { const enhancedUser = await this.enhanceUser(user); callback(enhancedUser); } else { callback(null); } }); } // Enhance Firebase user with custom claims and additional data private async enhanceUser(user: FirebaseUser): Promise { const tokenResult = await user.getIdTokenResult(); const customClaims = tokenResult.claims; // Cache custom claims this.customClaimsCache.set(user.uid, customClaims); const enhancedUser: AuthUser = { ...user, customClaims, role: customClaims.role as string | undefined, roles: customClaims.roles as string[] | undefined, department: customClaims.department as string | undefined, departments: customClaims.departments as string[] | undefined, isAdmin: customClaims.isAdmin as boolean | undefined, isSuperAdmin: customClaims.isSuperAdmin as boolean | undefined, permissions: customClaims.permissions as string[] | undefined, }; return enhancedUser; } // Get cached enhanced user private getCachedEnhancedUser(user: FirebaseUser): AuthUser { const customClaims = this.customClaimsCache.get(user.uid) || {}; return { ...user, customClaims, role: customClaims.role as string | undefined, permissions: customClaims.permissions as string[] | undefined, department: customClaims.department as string | undefined, }; } // Check if user is new (created within last minute) private isNewUser(user: FirebaseUser): boolean { if (!user.metadata.creationTime) return false; const creationTime = new Date(user.metadata.creationTime).getTime(); const now = Date.now(); const oneMinute = 60 * 1000; return now - creationTime < oneMinute; } // Set user metadata (requires server-side implementation) private async setUserMetadata(_uid: string, _metadata: any): Promise { // This would typically call your backend API to set custom claims console.warn('setUserMetadata requires server-side implementation'); } // Normalize Firebase errors to consistent format private normalizeError(error: any): AuthError { const errorCode = error.code || 'unknown'; let message = error.message || 'An unknown error occurred'; // Provide user-friendly error messages switch (errorCode) { case 'auth/invalid-email': message = 'The email address is not valid.'; break; case 'auth/user-disabled': message = 'This user account has been disabled.'; break; case 'auth/user-not-found': message = 'No user found with this email address.'; break; case 'auth/wrong-password': message = 'The password is incorrect.'; break; case 'auth/email-already-in-use': message = 'An account already exists with this email address.'; break; case 'auth/weak-password': message = 'The password must be at least 6 characters long.'; break; case 'auth/popup-closed-by-user': message = 'The sign-in popup was closed before completing sign in.'; break; } return { code: errorCode, message, originalError: error, }; } // Get Firebase Auth instance (for advanced usage) getAuth(): FirebaseAuth { return this.auth; } // Get Firebase App instance getApp(): FirebaseApp { return this.app; } } // Singleton instance let authInstance: AuthService | null = null; // Initialize auth service export function initAuth(config: AuthConfig): AuthService { if (!authInstance) { authInstance = new AuthService(config); authInstance.initialize(); } return authInstance; } // Get auth instance export function getAuthInstance(): AuthService { if (!authInstance) { throw new Error('Auth not initialized. Call initAuth first.'); } return authInstance; }