UNPKG

@commit451/salamander

Version:

Never be AFK

330 lines 14.4 kB
import { input } from '@inquirer/prompts'; import chalk from 'chalk'; import { createServer } from 'http'; import { URL } from 'url'; import { exec } from 'child_process'; import { promisify } from 'util'; import { clearAuth, loadAuth, saveAuth } from '../utils/storage.js'; import { getAuth, GoogleAuthProvider, onAuthStateChanged, signInWithCredential } from 'firebase/auth'; import app from '../config/firebase.js'; const execAsync = promisify(exec); const auth = getAuth(app); export class AuthService { static currentUser = null; static customUserInfo = null; static authStateInitialized = false; static authStateReady = null; static CLIENT_ID = '87955960620-mu7hdiu5nntb4al1ekk79dn73bu5otvu.apps.googleusercontent.com'; static async openBrowser(url) { const platform = process.platform; let command; switch (platform) { case 'darwin': command = `open "${url}"`; break; case 'win32': command = `start "${url}"`; break; default: command = `xdg-open "${url}"`; } try { await execAsync(command); } catch (error) { console.log(chalk.yellow(`⚠️ Could not open browser automatically. Please visit: ${url}`)); } } static async startLocalServer() { return new Promise((resolve, reject) => { const server = createServer(); let authCodeResolve; let authCodeReject; const authCodePromise = new Promise((res, rej) => { authCodeResolve = res; authCodeReject = rej; }); server.on('request', (req, res) => { if (req.url) { const url = new URL(req.url, 'http://localhost:8080'); if (url.pathname === '/oauth/callback') { const code = url.searchParams.get('code'); const error = url.searchParams.get('error'); if (error) { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end('<h1>Authentication Failed</h1><p>You can close this window.</p>'); authCodeReject(new Error(`OAuth error: ${error}`)); return; } if (code) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <!DOCTYPE html> <html> <head> <title>Authentication Successful</title> <style> body { background-color: #000000; color: #ffffff; font-family: Arial, sans-serif; text-align: center; padding: 50px; margin: 0; } h1 { font-size: 2.5em; margin-bottom: 20px; } p { font-size: 1.2em; } </style> </head> <body> <h1>Authentication Successful!</h1> <p>You can close this window and return to the CLI.</p> </body> </html> `); authCodeResolve(code); } else { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end('<h1>Authentication Failed</h1><p>No authorization code received.</p>'); authCodeReject(new Error('No authorization code received')); } } } }); server.listen(8080, 'localhost', () => { resolve({ server, authCodePromise }); }); server.on('error', (error) => { reject(error); }); }); } static async exchangeCodeForTokens(code) { const params = new URLSearchParams({ client_id: this.CLIENT_ID, code, grant_type: 'authorization_code', redirect_uri: 'http://localhost:8080/oauth/callback', }); const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { const error = await response.text(); throw new Error(`Token exchange failed: ${error}`); } return await response.json(); } static async refreshToken(refreshToken) { const params = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.CLIENT_ID, }); const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Token refresh failed: ${response.status} ${errorText}`); } const tokens = await response.json(); // Sign in to Firebase with the new access token const credential = GoogleAuthProvider.credential(null, tokens.access_token); await signInWithCredential(auth, credential); // Update stored tokens with new access token (refresh token may be the same) const existingAuth = await loadAuth(); await saveAuth({ userId: existingAuth?.userId || '', email: existingAuth?.email || '', accessToken: tokens.access_token, refreshToken: tokens.refresh_token || existingAuth?.refreshToken }); } static async initialize() { if (this.authStateInitialized) { // Wait for auth state to be ready if initialization is in progress if (this.authStateReady) { await this.authStateReady; } return; } // Create a promise that resolves when auth state is determined let resolveAuthState; this.authStateReady = new Promise((resolve) => { resolveAuthState = resolve; }); // Try to restore from stored auth const storedAuth = await loadAuth(); let authAttemptMade = false; if (storedAuth?.userId && storedAuth.email && storedAuth.accessToken) { authAttemptMade = true; try { // Try to restore Firebase session using stored access token const credential = GoogleAuthProvider.credential(null, storedAuth.accessToken); await signInWithCredential(auth, credential); console.log(chalk.green('✅ Session restored from storage')); } catch (error) { // Token might be expired, try refresh token if available if (storedAuth.refreshToken) { try { await this.refreshToken(storedAuth.refreshToken); console.log(chalk.green('✅ Session refreshed from storage')); } catch (refreshError) { console.log(chalk.yellow('⚠️ Stored session expired, please sign in again')); await clearAuth(); authAttemptMade = false; } } else { console.log(chalk.yellow('⚠️ Stored session expired, please sign in again')); await clearAuth(); authAttemptMade = false; } } } // Set up Firebase Auth state observer onAuthStateChanged(auth, async (user) => { if (user) { this.currentUser = user; if (!this.customUserInfo) { this.customUserInfo = { id: user.uid, email: user.email || '', name: user.displayName || user.email?.split('@')[0] || '' }; } // Only update user info in storage, preserve OAuth tokens if they exist const existingAuth = await loadAuth(); await saveAuth({ userId: user.uid, email: user.email || '', accessToken: existingAuth?.accessToken || undefined, refreshToken: existingAuth?.refreshToken || undefined }); } else { // User signed out, clean up this.currentUser = null; this.customUserInfo = null; await clearAuth(); } // Resolve the auth state promise once we know the state if (resolveAuthState) { resolveAuthState(); resolveAuthState = null; } }); this.authStateInitialized = true; // If no auth attempt was made, resolve immediately since no user will be set if (!authAttemptMade) { setTimeout(() => { if (resolveAuthState) { resolveAuthState(); } }, 100); } // Wait for auth state to be determined await this.authStateReady; } static get isAuthenticated() { return this.currentUser !== null; } static get user() { return this.currentUser; } static get userId() { return this.currentUser?.uid || null; } static async loginFlow() { console.log(chalk.blue('\n🔐 Authentication Required')); console.log('Please sign in with Google to continue.\n'); try { // Ensure auth state is initialized await this.initialize(); // Check if user is already authenticated if (this.isAuthenticated) { console.log(chalk.green('✅ Already signed in!')); return true; } const proceed = await input({ message: 'Press Enter to open Google sign-in in your browser, or type "q" to quit:', default: '', }); if (proceed.toLowerCase() === 'q') { return false; } console.log(chalk.yellow('🌐 Starting local server and opening browser...')); // Start local server to receive callback const { server, authCodePromise } = await this.startLocalServer(); try { // Create OAuth URL (simplified without PKCE) const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth'); authUrl.searchParams.set('client_id', this.CLIENT_ID); authUrl.searchParams.set('redirect_uri', 'http://localhost:8080/oauth/callback'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid email profile'); authUrl.searchParams.set('access_type', 'offline'); authUrl.searchParams.set('prompt', 'consent'); // Open browser await this.openBrowser(authUrl.toString()); console.log('Please complete the authentication in your browser...\n'); // Wait for callback const authCode = await authCodePromise; console.log(chalk.green('✅ Authorization code received!')); // Exchange code for tokens console.log('Exchanging authorization code for tokens...'); const tokens = await this.exchangeCodeForTokens(authCode); // Sign in to Firebase Auth using the Google OAuth token const credential = GoogleAuthProvider.credential(null, tokens.access_token); const userCredential = await signInWithCredential(auth, credential); this.currentUser = userCredential.user; // Store the OAuth tokens immediately after successful login await saveAuth({ userId: userCredential.user.uid, email: userCredential.user.email || '', accessToken: tokens.access_token, refreshToken: tokens.refresh_token }); console.log(chalk.green(`✅ Welcome ${userCredential.user.displayName || userCredential.user.email}!`)); console.log(chalk.green('✅ Login successful!')); return true; } finally { server.close(); } } catch (error) { console.error(chalk.red('❌ Authentication failed:'), error.message); return false; } } static async signOut() { try { await auth.signOut(); // Firebase auth state observer will handle clearing user state console.log(chalk.green('✅ Signed out successfully')); } catch (error) { console.error(chalk.red('❌ Error signing out:'), error.message); throw error; } } } //# sourceMappingURL=auth.js.map