-
Notifications
You must be signed in to change notification settings - Fork 51
CLI: Implement auth login command
#2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from 1 commit
999b2a8
8650b02
fc661e0
c8b228c
9fcc45f
36e6059
a228ae9
c1799fe
23aa26c
49966e3
e13fd34
c72a70d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { password } from '@inquirer/prompts'; | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { SupportedLocale } from 'common/lib/locale'; | ||
| import { getAuthenticationUrl } from 'common/lib/oauth'; | ||
| import { AuthCommandLoggerAction as LoggerAction } from 'common/logger-actions'; | ||
| import wpcomFactory from 'src/lib/wpcom-factory'; | ||
| import wpcomXhrRequest from 'src/lib/wpcom-xhr-request-factory'; | ||
| import { z } from 'zod'; | ||
| import { validateAccessToken } from 'cli/lib/api'; | ||
| import { lockAppdata, readAppdata, saveAppdata, unlockAppdata } from 'cli/lib/appdata'; | ||
| import { openBrowser } from 'cli/lib/browser'; | ||
| import { Logger, LoggerError } from 'cli/logger'; | ||
| import { StudioArgv } from 'cli/types'; | ||
|
|
||
| const meResponseSchema = z.object( { | ||
| ID: z.number(), | ||
| email: z.string().email(), | ||
| display_name: z.string(), | ||
| } ); | ||
|
|
||
| const CLI_REDIRECT_URI = `https://developer.wordpress.com/copy-oauth-token`; | ||
|
|
||
| export async function runCommand( locale: SupportedLocale = 'en' ): Promise< void > { | ||
| const logger = new Logger< LoggerAction >(); | ||
|
|
||
| try { | ||
| const existingData = await readAppdata(); | ||
| if ( existingData.authToken?.accessToken ) { | ||
| await validateAccessToken( existingData.authToken.accessToken ); | ||
| logger.reportSuccess( __( 'Already authenticated with WordPress.com' ) ); | ||
| return; | ||
| } | ||
|
|
||
| logger.reportStart( LoggerAction.LOGIN, __( 'Opening browser for authentication…' ) ); | ||
|
|
||
| const authUrl = getAuthenticationUrl( locale, CLI_REDIRECT_URI ); | ||
| await openBrowser( authUrl ); | ||
| logger.reportSuccess( __( 'Browser opened successfully' ) ); | ||
|
|
||
| console.log( __( 'Please complete authentication in your browser.' ) ); | ||
| console.log( '' ); | ||
|
|
||
| const accessToken = await password( { | ||
| message: __( 'Authentication token:' ), | ||
| } ); | ||
|
|
||
| logger.reportSuccess( __( 'Authentication completed successfully!' ) ); | ||
|
|
||
| const wpcom = wpcomFactory( accessToken, wpcomXhrRequest ); | ||
| const rawResponse = await wpcom.req.get( '/me', { fields: 'ID,login,email,display_name' } ); | ||
| const user = meResponseSchema.parse( rawResponse ); | ||
|
|
||
| const now = new Date(); | ||
| const twoWeeksInSeconds = 2 * 7 * 24 * 60 * 60; | ||
|
|
||
| try { | ||
| await lockAppdata(); | ||
| const userData = await readAppdata(); | ||
| userData.authToken = { | ||
| accessToken, | ||
| id: user.ID, | ||
| email: user.email, | ||
| displayName: user.display_name, | ||
| expiresIn: twoWeeksInSeconds, | ||
| expirationTime: now.getTime() + twoWeeksInSeconds, | ||
| }; | ||
| await saveAppdata( userData ); | ||
| } finally { | ||
| await unlockAppdata(); | ||
| } | ||
|
|
||
| logger.reportKeyValuePair( 'email', user.email ); | ||
| logger.reportKeyValuePair( 'display_name', user.display_name ); | ||
| } catch ( error ) { | ||
| if ( error instanceof LoggerError ) { | ||
| logger.reportError( error ); | ||
| } else { | ||
| logger.reportError( new LoggerError( __( 'Authentication failed' ), error ) ); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| export const registerCommand = ( yargs: StudioArgv ) => { | ||
| return yargs.command( { | ||
| command: 'login', | ||
| describe: __( 'Log in to WordPress.com' ), | ||
| builder: ( yargs ) => { | ||
| return yargs.option( 'locale', { | ||
| type: 'string', | ||
| default: 'en', | ||
| description: __( 'Locale for the authentication flow' ), | ||
| } ); | ||
| }, | ||
| handler: async ( argv ) => { | ||
| await runCommand( argv.locale as SupportedLocale ); | ||
| }, | ||
| } ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||
| import { spawn } from 'child_process'; | ||||
| import { __ } from '@wordpress/i18n'; | ||||
| import { LoggerError } from 'cli/logger'; | ||||
|
|
||||
|
||||
| import { LoggerError } from 'cli/logger'; |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variables cmd and args are not initialized and will be undefined if process.platform doesn't match 'darwin', 'win32', or 'linux'. This will cause the spawn() call on line 37 to fail. Add a default case to the switch statement that throws an error for unsupported platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have yet to test this. This was taken from Sindre Sorhus's open module.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| // Store the actions in a separate file to avoid Webpack issues when importing them in the Studio | ||
| // source code | ||
|
|
||
| export enum AuthCommandLoggerAction { | ||
| LOGIN = 'login', | ||
| LOGOUT = 'logout', | ||
|
||
| } | ||
|
|
||
| export enum PreviewCommandLoggerAction { | ||
| VALIDATE = 'validate', | ||
| ARCHIVE = 'archive', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
expirationTimecalculation is incorrect.twoWeeksInSecondsis in seconds (1209600), butgetTime()returns milliseconds. This should benow.getTime() + twoWeeksInSeconds * 1000to convert seconds to milliseconds, or the calculation should usetwoWeeksInSeconds * 1000directly.