@vizzly-testing/cli
Version:
Visual review platform for UI developers and designers
183 lines (163 loc) • 6.79 kB
JavaScript
/**
* Login command implementation
* Authenticates user via OAuth device flow
*/
import { completeDeviceFlow, createAuthClient, createTokenStore, initiateDeviceFlow, pollDeviceAuthorization } from '../auth/index.js';
import { openBrowser } from '../utils/browser.js';
import { getApiUrl } from '../utils/environment-config.js';
import * as output from '../utils/output.js';
/**
* Login command implementation using OAuth device flow
* @param {Object} options - Command options
* @param {Object} globalOptions - Global CLI options
*/
export async function loginCommand(options = {}, globalOptions = {}) {
output.configure({
json: globalOptions.json,
verbose: globalOptions.verbose,
color: !globalOptions.noColor
});
let colors = output.getColors();
try {
output.header('login');
// Create auth client and token store
let client = createAuthClient({
baseUrl: options.apiUrl || getApiUrl()
});
let tokenStore = createTokenStore();
// Initiate device flow
output.startSpinner('Connecting to Vizzly...');
let deviceFlow = await initiateDeviceFlow(client);
output.stopSpinner();
// Handle both snake_case and camelCase field names
let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
let userCode = deviceFlow.user_code || deviceFlow.userCode;
let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
if (!verificationUri || !userCode || !deviceCode) {
throw new Error('Invalid device flow response from server');
}
// Build URL with pre-filled code
let urlWithCode = `${verificationUri}?code=${userCode}`;
// Display user code prominently in a box
output.printBox(['Visit this URL to authorize:', '', colors.brand.info(urlWithCode), '', 'Your code:', '', colors.bold(colors.brand.amber(userCode))], {
title: 'Authorization',
style: 'branded'
});
output.blank();
// Try to open browser with pre-filled code
let browserOpened = await openBrowser(urlWithCode);
if (browserOpened) {
output.complete('Browser opened');
} else {
output.warn('Could not open browser automatically');
output.hint('Please open the URL manually');
}
output.blank();
output.hint('After authorizing, press Enter to continue...');
// Wait for user to press Enter
await new Promise(resolve => {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
resolve();
});
});
// Check authorization status
output.startSpinner('Checking authorization...');
let pollResponse = await pollDeviceAuthorization(client, deviceCode);
output.stopSpinner();
let tokenData = null;
// Check if authorization was successful by looking for tokens
if (pollResponse.tokens?.accessToken) {
// Success! We got tokens
tokenData = pollResponse;
} else if (pollResponse.status === 'pending') {
throw new Error('Authorization not complete yet. Please complete the authorization in your browser and try running "vizzly login" again.');
} else if (pollResponse.status === 'expired') {
throw new Error('Device code expired. Please try logging in again.');
} else if (pollResponse.status === 'denied') {
throw new Error('Authorization denied. Please try logging in again.');
} else {
throw new Error('Unexpected response from authorization server. Please try logging in again.');
}
// Complete device flow and save tokens
// Handle both snake_case and camelCase for token data, and nested tokens object
let tokensData = tokenData.tokens || tokenData;
let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
let tokens = {
accessToken: tokensData.accessToken || tokensData.access_token,
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
expiresAt: tokenExpiresAt,
user: tokenData.user,
organizations: tokenData.organizations
};
await completeDeviceFlow(tokenStore, tokens);
// Display success
output.complete('Authenticated');
output.blank();
// Show user info
if (tokens.user) {
output.keyValue({
User: tokens.user.name || tokens.user.username,
Email: tokens.user.email
});
}
// Show organization info
if (tokens.organizations && tokens.organizations.length > 0) {
output.blank();
output.labelValue('Organizations', '');
let orgItems = tokens.organizations.map(org => `${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
output.list(orgItems);
}
// Show token expiry info
if (tokens.expiresAt) {
output.blank();
let expiresAt = new Date(tokens.expiresAt);
let msUntilExpiry = expiresAt.getTime() - Date.now();
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
if (daysUntilExpiry > 0) {
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`);
} else if (hoursUntilExpiry > 0) {
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
} else if (minutesUntilExpiry > 0) {
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
}
}
output.blank();
output.hint('You can now use Vizzly CLI commands without VIZZLY_TOKEN');
output.cleanup();
} catch (error) {
output.stopSpinner();
// Handle authentication errors with helpful messages
if (error.name === 'AuthError') {
output.error('Authentication failed', error);
output.blank();
output.hint('Please try logging in again');
output.hint("If you don't have an account, sign up at https://vizzly.dev");
process.exit(1);
} else if (error.code === 'RATE_LIMIT_ERROR') {
output.error('Too many login attempts', error);
output.blank();
output.hint('Please wait a few minutes before trying again');
process.exit(1);
} else {
output.error('Login failed', error);
process.exit(1);
}
}
}
/**
* Validate login options
* @param {Object} options - Command options
*/
export function validateLoginOptions() {
const errors = [];
// No specific validation needed for login command
// OAuth device flow handles everything via browser
return errors;
}