@mvp-factory/holy-auth-firebase
Version:
Firebase Authentication module with Google Sign-In support
270 lines (269 loc) • 9.59 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FirebaseAuthManager = void 0;
const app_1 = require("firebase/app");
const auth_1 = require("firebase/auth");
const FirebaseAuth_1 = require("../types/FirebaseAuth");
class FirebaseAuthManager {
constructor(options = {}) {
this.app = null;
this.auth = null;
this.config = null;
this.eventListeners = new Map();
this.authState = {
isAuthenticated: false,
isLoading: true,
user: null,
token: null,
error: null
};
this.unsubscribeAuthStateChanged = null;
this.options = {
persistence: 'local',
autoSync: true,
syncEndpoint: '/api/auth/firebase/sync',
...options
};
}
static getInstance(options) {
if (!FirebaseAuthManager.instance) {
FirebaseAuthManager.instance = new FirebaseAuthManager(options);
}
return FirebaseAuthManager.instance;
}
async initialize(config) {
if (this.app) {
console.warn('Firebase Auth already initialized');
return;
}
try {
this.config = config;
this.app = (0, app_1.initializeApp)(config);
this.auth = (0, auth_1.getAuth)(this.app);
await this.setPersistenceMode();
this.setupAuthStateListener();
console.log('✅ Firebase Auth initialized successfully');
}
catch (error) {
console.error('❌ Firebase Auth initialization failed:', error);
throw this.createAuthError(error);
}
}
async setPersistenceMode() {
if (!this.auth)
return;
const persistenceMap = {
local: auth_1.browserLocalPersistence,
session: auth_1.browserSessionPersistence,
none: auth_1.inMemoryPersistence
};
const persistence = persistenceMap[this.options.persistence || 'local'];
await (0, auth_1.setPersistence)(this.auth, persistence);
}
setupAuthStateListener() {
if (!this.auth)
return;
this.unsubscribeAuthStateChanged = (0, auth_1.onAuthStateChanged)(this.auth, async (firebaseUser) => {
if (firebaseUser) {
const user = await this.mapFirebaseUser(firebaseUser);
const token = await firebaseUser.getIdToken();
this.updateAuthState({
isAuthenticated: true,
isLoading: false,
user,
token,
error: null
});
if (this.options.autoSync) {
await this.syncWithBackend(user, token);
}
this.emit(FirebaseAuth_1.AuthEvent.AUTH_STATE_CHANGED, { user, token });
}
else {
this.updateAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
token: null,
error: null
});
this.emit(FirebaseAuth_1.AuthEvent.AUTH_STATE_CHANGED, { user: null, token: null });
}
});
}
async signInWithGoogle(customParameters) {
if (!this.auth) {
throw new Error('Firebase Auth not initialized');
}
try {
const provider = new auth_1.GoogleAuthProvider();
if (customParameters || this.options.customParameters) {
const params = { ...this.options.customParameters, ...customParameters };
Object.entries(params).forEach(([key, value]) => {
provider.setCustomParameters({ [key]: value });
});
}
if (this.options.scopes) {
this.options.scopes.forEach(scope => {
provider.addScope(scope);
});
}
const result = await (0, auth_1.signInWithPopup)(this.auth, provider);
const user = await this.mapFirebaseUser(result.user);
const token = await result.user.getIdToken();
const signInResult = {
user,
token,
credential: result.credential,
additionalUserInfo: result.additionalUserInfo
};
this.emit(FirebaseAuth_1.AuthEvent.SIGN_IN, signInResult);
return signInResult;
}
catch (error) {
const authError = this.createAuthError(error);
this.updateAuthState({ ...this.authState, error: authError });
throw authError;
}
}
async signOut() {
if (!this.auth) {
throw new Error('Firebase Auth not initialized');
}
try {
await (0, auth_1.signOut)(this.auth);
this.clearLocalStorage();
this.emit(FirebaseAuth_1.AuthEvent.SIGN_OUT, null);
}
catch (error) {
throw this.createAuthError(error);
}
}
getCurrentUser() {
return this.authState.user;
}
getAuthState() {
return { ...this.authState };
}
async getIdToken(forceRefresh = false) {
if (!this.auth?.currentUser) {
return null;
}
try {
const token = await this.auth.currentUser.getIdToken(forceRefresh);
if (forceRefresh) {
this.updateAuthState({ ...this.authState, token });
this.emit(FirebaseAuth_1.AuthEvent.TOKEN_REFRESH, { token });
}
return token;
}
catch (error) {
throw this.createAuthError(error);
}
}
isAuthenticated() {
return this.authState.isAuthenticated;
}
async syncWithBackend(user, token) {
if (!this.options.syncEndpoint)
return;
try {
const syncData = {
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
phoneNumber: user.phoneNumber,
providerId: user.providerId
};
const response = await fetch(this.options.syncEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(syncData),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Sync failed with status: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Sync failed');
}
console.log('✅ Backend session synchronized successfully');
}
catch (error) {
console.error('❌ Backend sync failed:', error);
}
}
async mapFirebaseUser(firebaseUser) {
return {
uid: firebaseUser.uid,
email: firebaseUser.email,
displayName: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
emailVerified: firebaseUser.emailVerified,
phoneNumber: firebaseUser.phoneNumber,
isAnonymous: firebaseUser.isAnonymous,
providerId: firebaseUser.providerId,
metadata: {
creationTime: firebaseUser.metadata.creationTime,
lastSignInTime: firebaseUser.metadata.lastSignInTime
}
};
}
updateAuthState(newState) {
this.authState = newState;
if (this.options.persistence === 'local' && newState.user) {
localStorage.setItem('firebase_id_token', newState.token || '');
localStorage.setItem('user_email', newState.user.email || '');
localStorage.setItem('user_uid', newState.user.uid);
}
}
clearLocalStorage() {
localStorage.removeItem('firebase_id_token');
localStorage.removeItem('user_email');
localStorage.removeItem('user_uid');
}
createAuthError(error) {
const authError = new Error(error.message || 'Authentication error');
authError.code = error.code || 'unknown';
authError.customData = error.customData;
return authError;
}
on(event, listener) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set());
}
this.eventListeners.get(event).add(listener);
return () => {
this.eventListeners.get(event)?.delete(listener);
};
}
off(event, listener) {
this.eventListeners.get(event)?.delete(listener);
}
emit(event, data) {
this.eventListeners.get(event)?.forEach(listener => {
try {
listener(event, data);
}
catch (error) {
console.error(`Error in auth event listener for ${event}:`, error);
}
});
}
destroy() {
if (this.unsubscribeAuthStateChanged) {
this.unsubscribeAuthStateChanged();
}
this.eventListeners.clear();
this.auth = null;
this.app = null;
FirebaseAuthManager.instance = null;
}
}
exports.FirebaseAuthManager = FirebaseAuthManager;