diff --git a/src/actions/auth/login.ts b/src/actions/auth/login.ts index dcad3ec..5414e7a 100644 --- a/src/actions/auth/login.ts +++ b/src/actions/auth/login.ts @@ -246,12 +246,9 @@ async function promptAuthenticationMethod() { message: 'Choose authentication method:', choices: [ { - name: chalk.dim( - 'šŸ” OAuth (Google Account) - Interactive browser authentication' - ), + name: 'šŸ” OAuth (Google Account) - Interactive browser authentication', value: 'oauth', short: 'OAuth', - disabled: chalk.dim('Work in Progress'), }, { name: 'šŸ”‘ Service Account - JSON key file authentication', @@ -338,22 +335,20 @@ const loginAction = async (options: LoginActionType) => { const config = loadConfig(); let isReauthenticate = false; - if (options.method == 'oauth') { - console.log( - chalk.gray( - 'šŸ” oauth is not implemented yet. We are working on it. Please use service-account instead.\n' - ) - ); - return; - } + // Check if already authenticated (service account or OAuth) + const hasServiceAccount = + config.serviceAccountPath && fs.existsSync(config.serviceAccountPath); + const hasOAuth = + config.authMethod === 'oauth' && fs.existsSync(CREDENTIALS_FILE); - // Check if already authenticated with service account - if ( - config.serviceAccountPath && - fs.existsSync(config.serviceAccountPath) && - !options.force - ) { - console.log(chalk.green('āœ… Already authenticated with service account')); + if ((hasServiceAccount || hasOAuth) && !options.force) { + if (hasServiceAccount) { + console.log( + chalk.green('āœ… Already authenticated with service account') + ); + } else { + console.log(chalk.green('āœ… Already authenticated with OAuth')); + } if (config.defaultProject) { console.log( @@ -380,35 +375,110 @@ const loginAction = async (options: LoginActionType) => { console.log(chalk.blue('šŸ” Starting authentication process...\n')); - // Only support service account authentication - console.log(chalk.blue('šŸ”‘ Service Account Authentication\n')); - const serviceAccountPath = await promptServiceAccountFile(); + // Determine auth method: use CLI flag or prompt the user + let authMethod: LoginMethod; + if (options.method) { + authMethod = options.method; + } else { + authMethod = await promptAuthenticationMethod(); + } - const serviceAccount = JSON.parse( - fs.readFileSync(path.resolve(serviceAccountPath), 'utf8') - ); - console.log(chalk.green('āœ… Service account loaded successfully!')); - console.log(chalk.gray(` └── Project: ${serviceAccount.project_id}`)); - - // Save service account info to config for future use - const newConfig = { - ...config, - serviceAccountPath: path.resolve(serviceAccountPath), - defaultProject: serviceAccount.project_id, - }; - - saveConfig(newConfig); - console.log(chalk.green(`āœ… Service account saved for future use`)); - console.log( - chalk.green(`āœ… Default project set to: ${serviceAccount.project_id}`) - ); + if (authMethod === 'oauth') { + const credentials = await authenticateWithOAuth(); - console.log( - chalk.green('\nšŸŽ‰ Setup complete! You can now use all commands.') - ); - console.log( - chalk.gray('šŸ’” No need to specify --service-account flag anymore') - ); + console.log(chalk.green('\nāœ… OAuth authentication successful!')); + console.log(chalk.gray(' └── Access token received')); + + // Try to fetch available Firebase projects and let the user pick one + let defaultProject: string | undefined; + try { + const { listUserProjects } = await import('@/actions/auth/projects'); + const projects = await listUserProjects(credentials); + + if (projects.length > 0) { + const projectChoices = projects.map((p) => ({ + name: `${p.name} (${p.projectId})`, + value: p.projectId, + short: p.projectId, + })); + + const { selectedProject } = await inquirer.prompt([ + { + type: 'list', + name: 'selectedProject', + message: 'Select a default Firebase project:', + choices: projectChoices, + }, + ]); + + defaultProject = selectedProject; + } else { + console.log( + chalk.yellow( + 'āš ļø No Firebase projects found for your account. You can set one later with: firebase-tools-cli projects --set-default ' + ) + ); + } + } catch (projectError) { + console.log( + chalk.yellow( + 'āš ļø Could not fetch projects automatically. You can set a default project later with: firebase-tools-cli projects --set-default ' + ) + ); + } + + // Save OAuth credentials and remove service account from config + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { serviceAccountPath: _removed, ...restConfig } = config; + const newConfig = { + ...restConfig, + authMethod: 'oauth' as const, + ...(defaultProject ? { defaultProject } : {}), + }; + + saveConfig(newConfig); + + console.log(chalk.green('\nāœ… OAuth credentials saved for future use')); + if (defaultProject) { + console.log( + chalk.green(`āœ… Default project set to: ${defaultProject}`) + ); + } + console.log( + chalk.green('\nšŸŽ‰ Setup complete! You can now use all commands.') + ); + } else { + // Service account authentication + console.log(chalk.blue('šŸ”‘ Service Account Authentication\n')); + const serviceAccountPath = await promptServiceAccountFile(); + + const serviceAccount = JSON.parse( + fs.readFileSync(path.resolve(serviceAccountPath), 'utf8') + ); + console.log(chalk.green('āœ… Service account loaded successfully!')); + console.log(chalk.gray(` └── Project: ${serviceAccount.project_id}`)); + + // Save service account info to config for future use + const newConfig = { + ...config, + authMethod: 'service-account' as const, + serviceAccountPath: path.resolve(serviceAccountPath), + defaultProject: serviceAccount.project_id, + }; + + saveConfig(newConfig); + console.log(chalk.green(`āœ… Service account saved for future use`)); + console.log( + chalk.green(`āœ… Default project set to: ${serviceAccount.project_id}`) + ); + + console.log( + chalk.green('\nšŸŽ‰ Setup complete! You can now use all commands.') + ); + console.log( + chalk.gray('šŸ’” No need to specify --service-account flag anymore') + ); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/actions/auth/projects.ts b/src/actions/auth/projects.ts index 8be27a3..ab289dd 100644 --- a/src/actions/auth/projects.ts +++ b/src/actions/auth/projects.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import { Credentials, OAuth2Client } from 'google-auth-library'; import path from 'path'; +import { CREDENTIALS_FILE } from '@/constants'; import { loadConfig, saveConfig } from '@/utils'; type ProjectType = { @@ -215,6 +216,59 @@ const listProjectsAction = async ( } } + // Check for saved OAuth credentials + if (config.authMethod === 'oauth' && fs.existsSync(CREDENTIALS_FILE)) { + let savedCredentials: Credentials; + try { + savedCredentials = JSON.parse( + fs.readFileSync(CREDENTIALS_FILE, 'utf8') + ); + } catch { + console.error(chalk.red('āŒ Saved OAuth credentials are corrupted')); + console.log(chalk.yellow('šŸ’” Try: firebase-tools-cli login --force')); + return; + } + + console.log(chalk.blue('šŸ” Using saved OAuth authentication')); + + const projects = await listUserProjects(savedCredentials); + + if (projects.length > 0) { + console.log(chalk.cyan('\nYour Firebase Projects:\n')); + projects.forEach((project) => { + const isDefault = project.projectId === config.defaultProject; + const marker = isDefault ? chalk.green(' āœ“ (default)') : ''; + console.log( + chalk.white(`šŸ“ ${project.name || project.projectId}`) + marker + ); + console.log(chalk.gray(` └── ID: ${project.projectId}`)); + console.log(chalk.gray(` └── Type: OAuth`)); + console.log(); + }); + } else { + if (config.defaultProject) { + console.log(chalk.cyan('Default Project:\n')); + console.log(chalk.white(`šŸ“ ${config.defaultProject}`)); + console.log(chalk.gray(` └── ID: ${config.defaultProject}`)); + console.log(chalk.gray(` └── Type: OAuth`)); + console.log(chalk.green(` └── Status: āœ“ (default)`)); + } else { + console.log(chalk.yellow('āš ļø No projects found for your account')); + } + } + + console.log(); + console.log(chalk.blue('šŸ’” Commands:')); + console.log( + chalk.gray(' • firebase-tools-cli projects --set-default ') + ); + console.log( + chalk.gray(' • firebase-tools-cli projects --clear-default') + ); + console.log(chalk.gray(' • firebase-tools-cli reset --config-only')); + return; + } + console.log(chalk.yellow('šŸ” No authentication found')); console.log(chalk.gray('Run: firebase-tools-cli login')); console.log( diff --git a/src/hooks/init.ts b/src/hooks/init.ts index d0d65bc..91af613 100644 --- a/src/hooks/init.ts +++ b/src/hooks/init.ts @@ -2,9 +2,11 @@ import chalk from 'chalk'; import { Command } from 'commander'; import * as admin from 'firebase-admin'; import fs from 'fs'; +import { Credentials, OAuth2Client } from 'google-auth-library'; import inquirer from 'inquirer'; import path from 'path'; +import { CREDENTIALS_FILE, OAUTH_CONFIG } from '@/constants'; import { loadConfig, saveConfig } from '@/utils'; async function promptServiceAccountFile() { @@ -103,6 +105,42 @@ async function configureAdminServiceAccount( return { db, credential, projectId }; } +async function configureAdminOAuth( + credentials: Credentials, + projectId: string +) { + const oauth2Client = new OAuth2Client( + OAUTH_CONFIG.clientId, + OAUTH_CONFIG.clientSecret, + OAUTH_CONFIG.redirectUri + ); + oauth2Client.setCredentials(credentials); + + const credential: admin.credential.Credential = { + getAccessToken: async () => { + const tokenResponse = await oauth2Client.getAccessToken(); + if (!tokenResponse.token) { + throw new Error('Failed to retrieve OAuth access token'); + } + const expiresIn = + (tokenResponse.res?.data as { expires_in?: number } | undefined) + ?.expires_in ?? 3600; + return { + access_token: tokenResponse.token, + expires_in: expiresIn, + }; + }, + }; + + console.log(chalk.blue(`šŸ” Using OAuth authentication`)); + console.log(chalk.gray(` └── Project: ${projectId}`)); + + admin.initializeApp({ credential, projectId }); + const db = admin.firestore(); + + return { db, credential, projectId }; +} + async function initializeFirebase(thisCommand: Command) { const commandName = thisCommand.args[0]; const skipAuthCommands = ['reset', 'logout', 'login', 'docs', 'convert']; @@ -153,6 +191,35 @@ async function initializeFirebase(thisCommand: Command) { return; } + // Check for saved OAuth credentials + if (config.authMethod === 'oauth' && fs.existsSync(CREDENTIALS_FILE)) { + let savedCredentials: Credentials; + try { + savedCredentials = JSON.parse( + fs.readFileSync(CREDENTIALS_FILE, 'utf8') + ); + } catch { + throw new Error( + 'Saved OAuth credentials are corrupted. Please run: firebase-tools-cli login --force' + ); + } + + projectIdValue = projectIdValue || config.defaultProject; + + if (!projectIdValue) { + console.log(chalk.yellow('āš ļø No default project configured')); + console.log( + chalk.gray( + ' Run: firebase-tools-cli projects --set-default ' + ) + ); + process.exit(1); + } + + await configureAdminOAuth(savedCredentials, projectIdValue); + return; + } + console.log(chalk.yellow('šŸ” No authentication found')); console.log( chalk.blue("Let's set up service account authentication...\n") diff --git a/src/utils.ts b/src/utils.ts index 2a62aee..6885654 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,8 +6,9 @@ import inquirer from 'inquirer'; import { CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE } from './constants'; type ConfigType = { - authMethod: string; + authMethod?: 'oauth' | 'service-account'; serviceAccountPath?: string; + defaultProject?: string; }; function countNodes(data: any, count: number = 0): number {