UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

183 lines (163 loc) 6.79 kB
/** * 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; }