Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 116 additions & 46 deletions src/actions/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -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 <projectId>'
)
);
}
} 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 <projectId>'
)
);
}

// 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);

Expand Down
54 changes: 54 additions & 0 deletions src/actions/auth/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 <projectId>')
);
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(
Expand Down
67 changes: 67 additions & 0 deletions src/hooks/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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 <projectId>'
)
);
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")
Expand Down
3 changes: 2 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down