@commit451/salamander
Version:
Never be AFK
330 lines • 14.4 kB
JavaScript
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