glass/src/features/common/services/authService.js

218 lines
8.7 KiB
JavaScript

const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');
const { BrowserWindow, shell } = require('electron');
const { getFirebaseAuth } = require('./firebaseClient');
const fetch = require('node-fetch');
const encryptionService = require('./encryptionService');
const migrationService = require('./migrationService');
const sessionRepository = require('../repositories/session');
const providerSettingsRepository = require('../repositories/providerSettings');
const userModelSelectionsRepository = require('../repositories/userModelSelections');
async function getVirtualKeyByEmail(email, idToken) {
if (!idToken) {
throw new Error('Firebase ID token is required for virtual key request');
}
const resp = await fetch('https://serverless-api-sf3o.vercel.app/api/virtual_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({ email: email.trim().toLowerCase() }),
redirect: 'follow',
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
console.error('[VK] API request failed:', json.message || 'Unknown error');
throw new Error(json.message || `HTTP ${resp.status}: Virtual key request failed`);
}
const vKey = json?.data?.virtualKey || json?.data?.virtual_key || json?.data?.newVKey?.slug;
if (!vKey) throw new Error('virtual key missing in response');
return vKey;
}
class AuthService {
constructor() {
this.currentUserId = 'default_user';
this.currentUserMode = 'local'; // 'local' or 'firebase'
this.currentUser = null;
this.isInitialized = false;
// This ensures the key is ready before any login/logout state change.
encryptionService.initializeKey(this.currentUserId);
this.initializationPromise = null;
sessionRepository.setAuthService(this);
providerSettingsRepository.setAuthService(this);
userModelSelectionsRepository.setAuthService(this);
}
initialize() {
if (this.isInitialized) return this.initializationPromise;
// --- Break the circular dependency ---
// Inject this authService instance into the session repository so it can be used
// without a direct `require` cycle.
sessionRepository.setAuthService(this);
// --- End of dependency injection ---
this.initializationPromise = new Promise((resolve) => {
const auth = getFirebaseAuth();
onAuthStateChanged(auth, async (user) => {
const previousUser = this.currentUser;
if (user) {
// User signed IN
console.log(`[AuthService] Firebase user signed in:`, user.uid);
this.currentUser = user;
this.currentUserId = user.uid;
this.currentUserMode = 'firebase';
// Clean up any zombie sessions from a previous run for this user.
await sessionRepository.endAllActiveSessions();
// ** Initialize encryption key for the logged-in user **
await encryptionService.initializeKey(user.uid);
// ** Check for and run data migration for the user **
// No 'await' here, so it runs in the background without blocking startup.
migrationService.checkAndRunMigration(user);
// Start background task to fetch and save virtual key
(async () => {
try {
const idToken = await user.getIdToken(true);
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(virtualKey);
}
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
} catch (error) {
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
}
})();
} else {
// User signed OUT
console.log(`[AuthService] No Firebase user.`);
if (previousUser) {
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(null);
}
}
this.currentUser = null;
this.currentUserId = 'default_user';
this.currentUserMode = 'local';
// End active sessions for the local/default user as well.
await sessionRepository.endAllActiveSessions();
// ** Initialize encryption key for the default/local user **
await encryptionService.initializeKey(this.currentUserId);
}
this.broadcastUserState();
if (!this.isInitialized) {
this.isInitialized = true;
console.log('[AuthService] Initialized and resolved initialization promise.');
resolve();
}
});
});
return this.initializationPromise;
}
async startFirebaseAuthFlow() {
try {
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
const authUrl = `${webUrl}/login?mode=electron`;
console.log(`[AuthService] Opening Firebase auth URL in browser: ${authUrl}`);
await shell.openExternal(authUrl);
return { success: true };
} catch (error) {
console.error('[AuthService] Failed to open Firebase auth URL:', error);
return { success: false, error: error.message };
}
}
async signInWithCustomToken(token) {
const auth = getFirebaseAuth();
try {
const userCredential = await signInWithCustomToken(auth, token);
console.log(`[AuthService] Successfully signed in with custom token for user:`, userCredential.user.uid);
// onAuthStateChanged will handle the state update and broadcast
} catch (error) {
console.error('[AuthService] Error signing in with custom token:', error);
throw error; // Re-throw to be handled by the caller
}
}
async signOut() {
const auth = getFirebaseAuth();
try {
// End all active sessions for the current user BEFORE signing out.
await sessionRepository.endAllActiveSessions();
await signOut(auth);
console.log('[AuthService] User sign-out initiated successfully.');
// onAuthStateChanged will handle the state update and broadcast,
// which will also re-evaluate the API key status.
} catch (error) {
console.error('[AuthService] Error signing out:', error);
}
}
broadcastUserState() {
const userState = this.getCurrentUser();
console.log('[AuthService] Broadcasting user state change:', userState);
BrowserWindow.getAllWindows().forEach(win => {
if (win && !win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send('user-state-changed', userState);
}
});
}
getCurrentUserId() {
return this.currentUserId;
}
getCurrentUser() {
const isLoggedIn = !!(this.currentUserMode === 'firebase' && this.currentUser);
if (isLoggedIn) {
return {
uid: this.currentUser.uid,
email: this.currentUser.email,
displayName: this.currentUser.displayName,
mode: 'firebase',
isLoggedIn: true,
//////// before_modelStateService ////////
// hasApiKey: this.hasApiKey // Always true for firebase users, but good practice
//////// before_modelStateService ////////
};
}
return {
uid: this.currentUserId, // returns 'default_user'
email: 'contact@pickle.com',
displayName: 'Default User',
mode: 'local',
isLoggedIn: false,
//////// before_modelStateService ////////
// hasApiKey: this.hasApiKey
//////// before_modelStateService ////////
};
}
}
const authService = new AuthService();
module.exports = authService;