From 6f6f9d7540fc064b3ffcbc05b56441f9b5c2efeb Mon Sep 17 00:00:00 2001 From: Giovanni Di Milia Date: Tue, 12 May 2020 17:40:46 -0400 Subject: [PATCH 01/15] fix(package): fix name of the package deploy name (#4435) --- packages/@ionic/cli/src/commands/package/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ionic/cli/src/commands/package/deploy.ts b/packages/@ionic/cli/src/commands/package/deploy.ts index 04dc2e8750..45a3db6bab 100644 --- a/packages/@ionic/cli/src/commands/package/deploy.ts +++ b/packages/@ionic/cli/src/commands/package/deploy.ts @@ -46,7 +46,7 @@ export class DeployCommand extends Command { const dashUrl = this.env.config.getDashUrl(); return { - name: 'build', + name: 'deploy', type: 'project', summary: 'Deploys a binary to a destination, such as an app store using Appflow', description: ` From 93c5bf5ba0cc0549a4bf61f534ee30d465bfe35e Mon Sep 17 00:00:00 2001 From: Lars Mikkelsen Date: Sat, 16 May 2020 13:05:28 -0400 Subject: [PATCH 02/15] fix(link): fix GitHub OAuth URL on WSL (#4442) --- packages/@ionic/cli/package.json | 2 +- packages/@ionic/cli/src/commands/link.ts | 2 +- packages/@ionic/cli/src/lib/open.ts | 5 ++--- packages/@ionic/cli/src/lib/sso.ts | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index d07958a026..a2712fc74f 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -55,7 +55,7 @@ "elementtree": "^0.1.7", "leek": "0.0.24", "lodash": "^4.17.5", - "open": "^7.0.0", + "open": "^7.0.4", "os-name": "^3.1.0", "semver": "^7.1.1", "split2": "^3.0.0", diff --git a/packages/@ionic/cli/src/commands/link.ts b/packages/@ionic/cli/src/commands/link.ts index fb41242865..89d0b39317 100644 --- a/packages/@ionic/cli/src/commands/link.ts +++ b/packages/@ionic/cli/src/commands/link.ts @@ -397,7 +397,7 @@ If you are having issues linking, please get in touch with our Support[^support- } const url = await userClient.oAuthGithubLogin(userId); - await openUrl(url, { encode: false }); + await openUrl(url); confirm = await this.env.prompt({ type: 'confirm', diff --git a/packages/@ionic/cli/src/lib/open.ts b/packages/@ionic/cli/src/lib/open.ts index 1237d72615..cb910a4120 100644 --- a/packages/@ionic/cli/src/lib/open.ts +++ b/packages/@ionic/cli/src/lib/open.ts @@ -4,12 +4,11 @@ const debug = Debug('ionic:lib:open'); export interface OpenUrlOptions { app?: string | readonly string[]; - encode?: boolean; } -export async function openUrl(target: string, { app, encode: url = true }: OpenUrlOptions = {}): Promise { +export async function openUrl(target: string, options: OpenUrlOptions = {}): Promise { const o = await import ('open'); - const p = await o(target, { wait: false, app, url }); + const p = await o(target, { ...options, wait: false, url: true }); const e = (err: Error) => debug('Error during open: %O', err); const n = p.on.bind(p); diff --git a/packages/@ionic/cli/src/lib/sso.ts b/packages/@ionic/cli/src/lib/sso.ts index 21c1cb1884..4c79899548 100644 --- a/packages/@ionic/cli/src/lib/sso.ts +++ b/packages/@ionic/cli/src/lib/sso.ts @@ -59,8 +59,7 @@ export abstract class OAuth2Flow { const authorizationParams = this.generateAuthorizationParameters(challenge); const authorizationUrl = `${this.authorizationUrl}?${qs.stringify(authorizationParams)}`; - // don't doubly encode params, since `stringify` already did it - await openUrl(authorizationUrl, { encode: false }); + await openUrl(authorizationUrl); const authorizationCode = await this.getAuthorizationCode(); const token = await this.getAccessToken(authorizationCode, verifier); From 4ff162281e327b4740e92513925b6d37b9dfb71e Mon Sep 17 00:00:00 2001 From: Masahiko Sakakibara Date: Mon, 18 May 2020 03:45:50 +0900 Subject: [PATCH 03/15] fix(start): do not prompt for project type when cloning (#4427) --- packages/@ionic/cli/src/commands/start.ts | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@ionic/cli/src/commands/start.ts b/packages/@ionic/cli/src/commands/start.ts index ffe7799850..7b021abc88 100644 --- a/packages/@ionic/cli/src/commands/start.ts +++ b/packages/@ionic/cli/src/commands/start.ts @@ -257,7 +257,17 @@ Use the ${input('--type')} option to start projects using older versions of Ioni return; } - const projectType = options['type'] ? String(options['type']) : await this.getProjectType(); + const cloned = isValidURL(inputs[1]); + + if (cloned) { + if (!options['git']) { + this.env.log.warn(`The ${input('--no-git')} option has no effect when cloning apps. Git must be used.`); + } + + options['git'] = true; + } + + const projectType = cloned ? 'custom' : options['type'] ? String(options['type']) : await this.getProjectType(); if (options['cordova']) { const { checkForUnsupportedProject } = await import('../lib/integrations/cordova/utils'); @@ -343,8 +353,6 @@ Use the ${input('--type')} option to start projects using older versions of Ioni inputs[1] = starterTemplate.repo; } - const cloned = isValidURL(inputs[1]); - if (this.project && this.project.details.context === 'app') { const confirm = await this.env.prompt({ type: 'confirm', @@ -361,14 +369,6 @@ Use the ${input('--type')} option to start projects using older versions of Ioni await this.validateProjectType(projectType); - if (cloned) { - if (!options['git']) { - this.env.log.warn(`The ${input('--no-git')} option has no effect when cloning apps. Git must be used.`); - } - - options['git'] = true; - } - if (options['v1'] || options['v2']) { throw new FatalException( `The ${input('--v1')} and ${input('--v2')} flags have been removed.\n` + @@ -703,7 +703,7 @@ Use the ${input('--type')} option to start projects using older versions of Ioni async validateProjectType(type: string) { const projectTypes = getStarterProjectTypes(); - if (!projectTypes.includes(type)) { + if (!['custom', ...projectTypes].includes(type)) { throw new FatalException( `${input(type)} is not a valid project type.\n` + `Please choose a different ${input('--type')}. Use ${input('ionic start --list')} to list all available starter templates.` From aaa9d49580c5f20b776831f807002a1b60583d9c Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Sun, 17 May 2020 12:07:17 -0700 Subject: [PATCH 04/15] chore(release): publish [skip ci] - @ionic/cli@6.9.2 - cli-scripts@2.1.15 --- packages/@ionic/cli/CHANGELOG.md | 13 +++++++++++++ packages/@ionic/cli/package.json | 2 +- packages/cli-scripts/CHANGELOG.md | 8 ++++++++ packages/cli-scripts/package.json | 4 ++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/@ionic/cli/CHANGELOG.md b/packages/@ionic/cli/CHANGELOG.md index 50790b75a4..3a7e626144 100644 --- a/packages/@ionic/cli/CHANGELOG.md +++ b/packages/@ionic/cli/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [6.9.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.1...@ionic/cli@6.9.2) (2020-05-17) + + +### Bug Fixes + +* **link:** fix GitHub OAuth URL on WSL ([#4442](https://github.com/ionic-team/ionic-cli/issues/4442)) ([93c5bf5](https://github.com/ionic-team/ionic-cli/commit/93c5bf5ba0cc0549a4bf61f534ee30d465bfe35e)) +* **package:** fix name of the package deploy name ([#4435](https://github.com/ionic-team/ionic-cli/issues/4435)) ([6f6f9d7](https://github.com/ionic-team/ionic-cli/commit/6f6f9d7540fc064b3ffcbc05b56441f9b5c2efeb)) +* **start:** do not prompt for project type when cloning ([#4427](https://github.com/ionic-team/ionic-cli/issues/4427)) ([4ff1622](https://github.com/ionic-team/ionic-cli/commit/4ff162281e327b4740e92513925b6d37b9dfb71e)) + + + + + ## [6.9.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.0...@ionic/cli@6.9.1) (2020-05-12) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index a2712fc74f..766d73cca1 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/cli", - "version": "6.9.1", + "version": "6.9.2", "description": "A tool for creating and developing Ionic Framework mobile apps.", "homepage": "https://ionicframework.com", "author": "Ionic Team (https://ionicframework.com) ", diff --git a/packages/cli-scripts/CHANGELOG.md b/packages/cli-scripts/CHANGELOG.md index b1c8a37156..ebd00dd8f4 100644 --- a/packages/cli-scripts/CHANGELOG.md +++ b/packages/cli-scripts/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.1.15](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.14...cli-scripts@2.1.15) (2020-05-17) + +**Note:** Version bump only for package cli-scripts + + + + + ## [2.1.14](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.13...cli-scripts@2.1.14) (2020-05-12) diff --git a/packages/cli-scripts/package.json b/packages/cli-scripts/package.json index 29b466145b..af88532f88 100644 --- a/packages/cli-scripts/package.json +++ b/packages/cli-scripts/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "cli-scripts", - "version": "2.1.14", + "version": "2.1.15", "bin": { "ionic-cli-scripts": "./bin/ionic-cli-scripts" }, @@ -16,7 +16,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@ionic/cli": "6.9.1", + "@ionic/cli": "6.9.2", "@ionic/cli-framework": "4.1.5", "@ionic/utils-fs": "3.1.3", "ansi-styles": "^4.0.0", From daf1f72372ca3966c57c7e32adc38001cf3a5fdb Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 27 May 2020 15:53:05 -0700 Subject: [PATCH 05/15] fix(start): fix issue with starting the react conf app --- packages/@ionic/cli/src/commands/start.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@ionic/cli/src/commands/start.ts b/packages/@ionic/cli/src/commands/start.ts index 7b021abc88..31a18ab691 100644 --- a/packages/@ionic/cli/src/commands/start.ts +++ b/packages/@ionic/cli/src/commands/start.ts @@ -257,17 +257,7 @@ Use the ${input('--type')} option to start projects using older versions of Ioni return; } - const cloned = isValidURL(inputs[1]); - - if (cloned) { - if (!options['git']) { - this.env.log.warn(`The ${input('--no-git')} option has no effect when cloning apps. Git must be used.`); - } - - options['git'] = true; - } - - const projectType = cloned ? 'custom' : options['type'] ? String(options['type']) : await this.getProjectType(); + const projectType = isValidURL(inputs[1]) ? 'custom' : options['type'] ? String(options['type']) : await this.getProjectType(); if (options['cordova']) { const { checkForUnsupportedProject } = await import('../lib/integrations/cordova/utils'); @@ -353,6 +343,8 @@ Use the ${input('--type')} option to start projects using older versions of Ioni inputs[1] = starterTemplate.repo; } + const cloned = isValidURL(inputs[1]); + if (this.project && this.project.details.context === 'app') { const confirm = await this.env.prompt({ type: 'confirm', @@ -369,6 +361,14 @@ Use the ${input('--type')} option to start projects using older versions of Ioni await this.validateProjectType(projectType); + if (cloned) { + if (!options['git']) { + this.env.log.warn(`The ${input('--no-git')} option has no effect when cloning apps. Git must be used.`); + } + + options['git'] = true; + } + if (options['v1'] || options['v2']) { throw new FatalException( `The ${input('--v1')} and ${input('--v2')} flags have been removed.\n` + From 9993aa578dac0af37812ac7e221fa41045bf272e Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 27 May 2020 15:54:01 -0700 Subject: [PATCH 06/15] chore(release): publish [skip ci] - @ionic/cli@6.9.3 - cli-scripts@2.1.16 --- packages/@ionic/cli/CHANGELOG.md | 11 +++++++++++ packages/@ionic/cli/package.json | 2 +- packages/cli-scripts/CHANGELOG.md | 8 ++++++++ packages/cli-scripts/package.json | 4 ++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/@ionic/cli/CHANGELOG.md b/packages/@ionic/cli/CHANGELOG.md index 3a7e626144..594685d3fc 100644 --- a/packages/@ionic/cli/CHANGELOG.md +++ b/packages/@ionic/cli/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [6.9.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.2...@ionic/cli@6.9.3) (2020-05-27) + + +### Bug Fixes + +* **start:** fix issue with starting the react conf app ([daf1f72](https://github.com/ionic-team/ionic-cli/commit/daf1f72372ca3966c57c7e32adc38001cf3a5fdb)) + + + + + ## [6.9.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.1...@ionic/cli@6.9.2) (2020-05-17) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index 766d73cca1..72afacd244 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/cli", - "version": "6.9.2", + "version": "6.9.3", "description": "A tool for creating and developing Ionic Framework mobile apps.", "homepage": "https://ionicframework.com", "author": "Ionic Team (https://ionicframework.com) ", diff --git a/packages/cli-scripts/CHANGELOG.md b/packages/cli-scripts/CHANGELOG.md index ebd00dd8f4..add4d07b90 100644 --- a/packages/cli-scripts/CHANGELOG.md +++ b/packages/cli-scripts/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.1.16](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.15...cli-scripts@2.1.16) (2020-05-27) + +**Note:** Version bump only for package cli-scripts + + + + + ## [2.1.15](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.14...cli-scripts@2.1.15) (2020-05-17) **Note:** Version bump only for package cli-scripts diff --git a/packages/cli-scripts/package.json b/packages/cli-scripts/package.json index af88532f88..126b6a991a 100644 --- a/packages/cli-scripts/package.json +++ b/packages/cli-scripts/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "cli-scripts", - "version": "2.1.15", + "version": "2.1.16", "bin": { "ionic-cli-scripts": "./bin/ionic-cli-scripts" }, @@ -16,7 +16,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@ionic/cli": "6.9.2", + "@ionic/cli": "6.9.3", "@ionic/cli-framework": "4.1.5", "@ionic/utils-fs": "3.1.3", "ansi-styles": "^4.0.0", From 70ac3ab633dc7dd65a2ac344c81a01f7914597b8 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 1 Jun 2020 12:19:20 -0700 Subject: [PATCH 07/15] feat(help): formatBeforeInputSummary and formatAfterInputSummary --- .../@ionic/cli-framework/src/definitions.ts | 1 - .../cli-framework/src/lib/__tests__/help.ts | 4 +- packages/@ionic/cli-framework/src/lib/help.ts | 59 +++++++++++++++---- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/@ionic/cli-framework/src/definitions.ts b/packages/@ionic/cli-framework/src/definitions.ts index 42c29b7d3f..405dba8576 100644 --- a/packages/@ionic/cli-framework/src/definitions.ts +++ b/packages/@ionic/cli-framework/src/definitions.ts @@ -15,7 +15,6 @@ export interface CommandMetadataInput { name: string; summary: string; validators?: Validator[]; - private?: boolean; } export interface TextFootnote { diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts b/packages/@ionic/cli-framework/src/lib/__tests__/help.ts index 9ecb02018c..f367d6d6fa 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/help.ts @@ -147,8 +147,8 @@ describe('@ionic/cli-framework', () => { Inputs: - input1 .......................... input1 summary - input2 .......................... input2 summary + input1 .......................... (optional) input1 summary + input2 .......................... (optional) input2 summary Options: diff --git a/packages/@ionic/cli-framework/src/lib/help.ts b/packages/@ionic/cli-framework/src/lib/help.ts index 0ee16e20ef..eb1f97b795 100644 --- a/packages/@ionic/cli-framework/src/lib/help.ts +++ b/packages/@ionic/cli-framework/src/lib/help.ts @@ -492,7 +492,7 @@ export class CommandStringHelpFormatter, N ext } async formatInputs(): Promise { - const { help: { title }, weak, input } = this.colors; + const { help: { title } } = this.colors; const metadata = await this.getCommandMetadata(); const inputs = metadata.inputs ? metadata.inputs : []; @@ -500,19 +500,28 @@ export class CommandStringHelpFormatter, N ext return ''; } - const fillStrings = generateFillSpaceStringList(inputs.map(i => i.name), this.dotswidth, weak('.')); - - const inputLineFn = ({ name, summary }: I, index: number) => { - const optionList = input(`${name}`); - const wrappedSummary = wordWrap(summary, { indentation: this.dotswidth + 6 }); - - return `${optionList} ${fillStrings[index]} ${wrappedSummary}`; - }; + const formattedInputs = await Promise.all(inputs.map(input => this.formatInput(input))); return ( `\n ${title('Inputs')}:` + - `\n\n ${inputs.map(inputLineFn).join('\n ')}\n` + `\n\n ${formattedInputs.join('\n ')}\n` + ); + } + + async formatInput(i: I): Promise { + const { input, weak } = this.colors; + const inputName = input(i.name); + const inputNameLength = stringWidth(inputName); + const fullLength = inputNameLength > this.dotswidth ? inputNameLength + 1 : this.dotswidth; + const fullDescription = ( + (await this.formatBeforeInputSummary(i)) + + i.summary + + (await this.formatAfterInputSummary(i)) ); + + const wrappedDescription = wordWrap(fullDescription, { indentation: this.dotswidth + 6 }); + + return `${inputName} ${weak('.').repeat(fullLength - inputNameLength)} ${wrappedDescription}`; } async formatOptionLine(opt: O) { @@ -534,7 +543,7 @@ export class CommandStringHelpFormatter, N ext } /** - * Insert text before the command's summary. + * Insert text that appears before the command's summary. * * @param meta The metadata of the command. */ @@ -543,7 +552,7 @@ export class CommandStringHelpFormatter, N ext } /** - * Insert text after the command's summary. + * Insert text that appears after the command's summary. * * @param meta The metadata of the command. */ @@ -551,6 +560,27 @@ export class CommandStringHelpFormatter, N ext return ''; } + /** + * Insert text that appears before the input's summary. + * + * @param input The metadata of the input. + */ + async formatBeforeInputSummary(input: I): Promise { + const { weak } = this.colors; + const required = input.validators && input.validators.includes(validators.required) ? true : false; + + return required ? '' : `${weak('(optional)')} `; + } + + /** + * Insert text that appears after the input's summary. + * + * @param input The metadata of the input. + */ + async formatAfterInputSummary(input: I): Promise { + return ''; + } + /** * Insert text that appears before an option's summary. * @@ -560,6 +590,11 @@ export class CommandStringHelpFormatter, N ext return formatHelpGroups(opt.groups, this.colors); } + /** + * Insert text that appears after the option's summary. + * + * @param opt The metadata of the option. + */ async formatAfterOptionSummary(opt: O): Promise { return this.formatOptionDefault(opt); } From 1bd30874f46d89248193c959dd8d769dd99503cf Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 1 Jun 2020 12:20:53 -0700 Subject: [PATCH 08/15] fix(cordova): platform is a required input --- packages/@ionic/cli/src/commands/cordova/run.ts | 5 +++-- packages/@ionic/cli/src/definitions.ts | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@ionic/cli/src/commands/cordova/run.ts b/packages/@ionic/cli/src/commands/cordova/run.ts index 1e9190ed3a..69548adc75 100644 --- a/packages/@ionic/cli/src/commands/cordova/run.ts +++ b/packages/@ionic/cli/src/commands/cordova/run.ts @@ -1,4 +1,4 @@ -import { Footnote, MetadataGroup } from '@ionic/cli-framework'; +import { Footnote, MetadataGroup, validators } from '@ionic/cli-framework'; import { onBeforeExit, sleepForever } from '@ionic/utils-process'; import * as Debug from 'debug'; import * as lodash from 'lodash'; @@ -149,6 +149,7 @@ Just like with ${input('ionic cordova build')}, you can pass additional options { name: 'platform', summary: `The platform to run (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`, + validators: [validators.required], }, ], options, @@ -213,7 +214,7 @@ Just like with ${input('ionic cordova build')}, you can pass additional options const [ platform ] = inputs; - if (options['native-run'] && !SUPPORTED_PLATFORMS.includes(platform)) { + if (platform && options['native-run'] && !SUPPORTED_PLATFORMS.includes(platform)) { this.env.log.warn(`${input(platform)} is not supported by ${input('native-run')}. Using Cordova to run the app.`); options['native-run'] = false; } diff --git a/packages/@ionic/cli/src/definitions.ts b/packages/@ionic/cli/src/definitions.ts index 41faef1761..ec41f58e62 100644 --- a/packages/@ionic/cli/src/definitions.ts +++ b/packages/@ionic/cli/src/definitions.ts @@ -4,7 +4,7 @@ import { CommandLineInputs, CommandLineOptions, CommandMetadata as FrameworkCommandMetadata, - CommandMetadataInput, + CommandMetadataInput as FrameworkCommandMetadataInput, CommandMetadataOption as FrameworkCommandMetadataOption, HydratedCommandMetadata as FrameworkHydratedCommandMetadata, ICommand as FrameworkCommand, @@ -22,7 +22,6 @@ import * as fs from 'fs'; export { CommandLineInputs, CommandLineOptions, - CommandMetadataInput, NamespaceMetadata, } from '@ionic/cli-framework'; @@ -363,6 +362,10 @@ export interface PackageVersions { [key: string]: string; } +export interface CommandMetadataInput extends FrameworkCommandMetadataInput { + private?: boolean; +} + export interface CommandMetadataOption extends FrameworkCommandMetadataOption { private?: boolean; hint?: string; From 2d74cb5a0243ec95a01af0e6fe795fab8ca45416 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 1 Jun 2020 14:54:48 -0700 Subject: [PATCH 09/15] docs(git): mark git commands as paid feature --- packages/@ionic/cli/src/commands/git/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@ionic/cli/src/commands/git/index.ts b/packages/@ionic/cli/src/commands/git/index.ts index de84b16920..59550c89ee 100644 --- a/packages/@ionic/cli/src/commands/git/index.ts +++ b/packages/@ionic/cli/src/commands/git/index.ts @@ -1,10 +1,13 @@ +import { MetadataGroup } from '@ionic/cli-framework'; + import { CommandMap, Namespace } from '../../lib/namespace'; export class GitNamespace extends Namespace { async getMetadata() { return { name: 'git', - summary: 'Commands relating to git', + summary: 'Commands relating to managing Appflow git', + groups: [MetadataGroup.PAID], }; } From d02f32d34c3ee8e1fa8bdabc41735b6004899e46 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 10:14:39 -0700 Subject: [PATCH 10/15] chore(deps-dev): bump @types/slice-ansi from 2.0.0 to 4.0.0 (#4457) Bumps [@types/slice-ansi](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/slice-ansi) from 2.0.0 to 4.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/slice-ansi) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@ionic/cli-framework/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ionic/cli-framework/package.json b/packages/@ionic/cli-framework/package.json index f7f51ef85b..6b3b7369b8 100644 --- a/packages/@ionic/cli-framework/package.json +++ b/packages/@ionic/cli-framework/package.json @@ -46,7 +46,7 @@ "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", "@types/node": "~10.17.13", - "@types/slice-ansi": "^2.0.0", + "@types/slice-ansi": "^4.0.0", "@types/wrap-ansi": "^3.0.0", "@types/write-file-atomic": "^3.0.0", "jest": "^25.1.0", From fc62c0c9d9388e8137ed632e22a25d0ebf789a9d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 10:45:51 -0700 Subject: [PATCH 11/15] chore(deps): bump uuid from 7.0.3 to 8.1.0 (#4450) Bumps [uuid](https://github.com/uuidjs/uuid) from 7.0.3 to 8.1.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v7.0.3...v8.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@ionic/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index 72afacd244..c401a9bc9f 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -66,7 +66,7 @@ "tar": "^6.0.1", "through2": "^3.0.0", "tslib": "1.11.2", - "uuid": "^7.0.1" + "uuid": "^8.1.0" }, "devDependencies": { "@types/debug": "^4.1.1", From 0738abd4123b6b5e078cd7ff2f76ed1a26474cf0 Mon Sep 17 00:00:00 2001 From: Giovanni Di Milia Date: Tue, 2 Jun 2020 16:36:51 -0400 Subject: [PATCH 12/15] feat(login): default to browser login (#4451) This new login flow uses the browser to authenticate via the OpenID spec. If you're already logged into the Ionic Hub, this will automatically authenticate your CLI session. The old login flow is available when using `ionic login` with the and arguments, e.g. `ionic login me@example.com my-secret-password`. This is no longer the recommended way to authenticate. SSO authentication via the `--sso` flag has been merged into the new login flow and the flag is no longer necessary. --- .../cli/assets/oauth/success/index.html | 106 +++++++++++++++ .../@ionic/cli/assets/sso/success/index.html | 51 ------- .../@ionic/cli/src/commands/deploy/build.ts | 2 +- .../@ionic/cli/src/commands/git/remote.ts | 2 +- packages/@ionic/cli/src/commands/link.ts | 4 +- packages/@ionic/cli/src/commands/login.ts | 112 +++++++++------ .../cli/src/commands/monitoring/syncmaps.ts | 2 +- .../@ionic/cli/src/commands/package/build.ts | 2 +- .../@ionic/cli/src/commands/package/deploy.ts | 2 +- packages/@ionic/cli/src/commands/ssh/add.ts | 2 +- .../@ionic/cli/src/commands/ssh/delete.ts | 4 +- .../@ionic/cli/src/commands/ssh/generate.ts | 2 +- packages/@ionic/cli/src/commands/ssh/list.ts | 2 +- packages/@ionic/cli/src/commands/start.ts | 2 +- packages/@ionic/cli/src/definitions.ts | 38 +++++- packages/@ionic/cli/src/guards.ts | 48 ++++++- packages/@ionic/cli/src/lib/config.ts | 11 +- .../cli/src/lib/doctor/ailments/index.ts | 2 +- packages/@ionic/cli/src/lib/http.ts | 29 +++- .../src/lib/integrations/enterprise/index.ts | 6 +- .../@ionic/cli/src/lib/{ => oauth}/auth.ts | 7 +- .../cli/src/lib/{sso.ts => oauth/oauth.ts} | 127 ++++++++---------- packages/@ionic/cli/src/lib/oauth/openid.ts | 64 +++++++++ packages/@ionic/cli/src/lib/session.ts | 112 ++++++++++----- packages/@ionic/cli/src/lib/telemetry.ts | 2 +- 25 files changed, 507 insertions(+), 234 deletions(-) create mode 100644 packages/@ionic/cli/assets/oauth/success/index.html delete mode 100644 packages/@ionic/cli/assets/sso/success/index.html rename packages/@ionic/cli/src/lib/{ => oauth}/auth.ts (81%) rename packages/@ionic/cli/src/lib/{sso.ts => oauth/oauth.ts} (51%) create mode 100644 packages/@ionic/cli/src/lib/oauth/openid.ts diff --git a/packages/@ionic/cli/assets/oauth/success/index.html b/packages/@ionic/cli/assets/oauth/success/index.html new file mode 100644 index 0000000000..67bdc47711 --- /dev/null +++ b/packages/@ionic/cli/assets/oauth/success/index.html @@ -0,0 +1,106 @@ + + + + + Success + + + +
+ + + + + +
+
+
+

You are authenticated.

+

Please return to your terminal. You may close this window.

+
+
+ + diff --git a/packages/@ionic/cli/assets/sso/success/index.html b/packages/@ionic/cli/assets/sso/success/index.html deleted file mode 100644 index 7737fb406d..0000000000 --- a/packages/@ionic/cli/assets/sso/success/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - Success - - - -
-

You are authenticated.

-

Please return to your terminal. You may close this window.

-
- - diff --git a/packages/@ionic/cli/src/commands/deploy/build.ts b/packages/@ionic/cli/src/commands/deploy/build.ts index 3148e86c77..47eab1988e 100644 --- a/packages/@ionic/cli/src/commands/deploy/build.ts +++ b/packages/@ionic/cli/src/commands/deploy/build.ts @@ -87,7 +87,7 @@ Apart from ${input('--commit')}, every option can be specified using the full na throw new FatalException(`Cannot run ${input('ionic deploy build')} outside a project directory.`); } - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const appflowId = await this.project.requireAppflowId(); if (!options.commit) { diff --git a/packages/@ionic/cli/src/commands/git/remote.ts b/packages/@ionic/cli/src/commands/git/remote.ts index ceb3e93119..0318ae1103 100644 --- a/packages/@ionic/cli/src/commands/git/remote.ts +++ b/packages/@ionic/cli/src/commands/git/remote.ts @@ -33,7 +33,7 @@ ${input('ionic git remote')} will check the local repository for whether or not throw new FatalException(`Cannot run ${input('ionic git remote')} outside a project directory.`); } - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const id = await this.project.requireAppflowId(); const appClient = new AppClient(token, this.env); const app = await appClient.load(id); diff --git a/packages/@ionic/cli/src/commands/link.ts b/packages/@ionic/cli/src/commands/link.ts index 89d0b39317..3aa1729774 100644 --- a/packages/@ionic/cli/src/commands/link.ts +++ b/packages/@ionic/cli/src/commands/link.ts @@ -230,13 +230,13 @@ If you are having issues linking, please get in touch with our Support[^support- private async getAppClient() { const { AppClient } = await import('../lib/app'); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); return new AppClient(token, this.env); } private async getUserClient() { const { UserClient } = await import('../lib/user'); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); return new UserClient(token, this.env); } diff --git a/packages/@ionic/cli/src/commands/login.ts b/packages/@ionic/cli/src/commands/login.ts index 123e59c36d..f7b2a90065 100644 --- a/packages/@ionic/cli/src/commands/login.ts +++ b/packages/@ionic/cli/src/commands/login.ts @@ -1,4 +1,4 @@ -import { MetadataGroup, combine, validators } from '@ionic/cli-framework'; +import { combine, validators } from '@ionic/cli-framework'; import * as chalk from 'chalk'; import * as readline from 'readline'; @@ -15,19 +15,23 @@ export class LoginCommand extends Command implements CommandPreRun { type: 'global', summary: 'Log in to Ionic', description: ` -Authenticate with Ionic and retrieve a user token, which is stored in the CLI config. The most secure way to log in is running ${input('ionic login')} without arguments, which will prompt you for your credentials. +Authenticate with Ionic and retrieve a user token, which is stored in the CLI config. The most secure way to log in is running ${input('ionic login')} without arguments, which will open a browser where you can submit your credentials. -If the ${input('IONIC_TOKEN')} environment variable is set, the CLI will automatically authenticate you. To retrieve your user token, first use ${input('ionic login')}, then print the token by running the ${input('ionic config get -g tokens.user')} command. +If the ${input('IONIC_TOKEN')} environment variable is set, the CLI will automatically authenticate you. To retrieve your user token, first use ${input('ionic login ')} to log in, then use ${input('ionic config get -g tokens.user')} to print the token. (${strong('Note')}: Tokens retrieved from the browser login are short-lived and not recommended for use with ${input('IONIC_TOKEN')}.) ${input('ionic login')} will also accept ${input('password')} through stdin, e.g.: ${input('echo "" | ionic login ')}. -If you need to create an Ionic account, use ${input('ionic signup')}. +If you need to create an Ionic account, use ${input('ionic signup')} or the Ionic Website[^signup]. You can reset your password in the Dashboard[^reset-password]. If you are having issues logging in, please get in touch with our Support[^support-request]. `, footnotes: [ + { + id: 'signup', + url: 'https://ionicframework.com/signup', + }, { id: 'reset-password', url: 'https://dashboard.ionicframework.com/reset-password', @@ -43,31 +47,18 @@ If you are having issues logging in, please get in touch with our Support[^suppo { name: 'email', summary: 'Your email address', - validators: [validators.required, validators.email], private: true, }, { name: 'password', summary: 'Your password', - // this is a hack since sso is hidden, no need to make password not required for it - validators: process.argv.includes('--sso') ? [] : [validators.required], private: true, }, ], - options: [ - { - name: 'sso', - type: Boolean, - summary: 'Open a window to log in with the SSO provider associated with your email', - groups: [MetadataGroup.HIDDEN], - }, - ], }; } async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { - const sso = !!options['sso']; - if (options['email'] || options['password']) { throw new FatalException( `${input('email')} and ${input('password')} are command arguments, not options. Please try this:\n` + @@ -75,13 +66,22 @@ If you are having issues logging in, please get in touch with our Support[^suppo ); } - const askForEmail = !inputs[0]; - const askForPassword = !sso && !inputs[1]; + if (options['sso']) { + this.env.log.warn( + `The ${strong('--sso')} flag is no longer necessary.\n` + + `SSO login has been upgraded to OpenID login, which is now the new default authentication flow of ${input('ionic login')}. Refresh tokens are used to automatically re-authenticate sessions.` + ); + this.env.log.nl(); + } + + // ask for password only if the user specifies an email + const validateEmail = !!inputs[0]; + const askForPassword = inputs[0] && !inputs[1]; if (this.env.session.isLoggedIn()) { const email = this.env.config.get('user.email'); - const extra = askForEmail || askForPassword + const extra = askForPassword ? (this.env.flags.interactive ? `Prompting for new credentials.\n\nUse ${chalk.yellow('Ctrl+C')} to cancel and remain logged in.` : '') : 'You will be logged out beforehand.'; @@ -105,18 +105,23 @@ If you are having issues logging in, please get in touch with our Support[^suppo } // TODO: combine with promptToLogin ? - - if (askForEmail) { - const email = await this.env.prompt({ - type: 'input', - name: 'email', - message: 'Email:', - validate: v => combine(validators.required, validators.email)(v), - }); - - inputs[0] = email; + if (validateEmail) { + const validatedEmail = validators.email(inputs[0]); + if (validatedEmail !== true) { + this.env.log.warn(`${validatedEmail}. \n Please enter a valid email address.`); + if (this.env.flags.interactive) { + const email = await this.env.prompt({ + type: 'input', + name: 'email', + message: 'Email:', + validate: v => combine(validators.required, validators.email)(v), + }); + inputs[0] = email; + } else { + throw new FatalException('Invalid email'); + } + } } - if (askForPassword) { if (this.env.flags.interactive) { const password = await this.env.prompt({ @@ -150,25 +155,44 @@ If you are having issues logging in, please get in touch with our Support[^suppo async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { const [ email, password ] = inputs; - const sso = !!options['sso']; - if (this.env.session.isLoggedIn()) { - await this.env.session.logout(); - this.env.config.set('tokens.telemetry', generateUUID()); - } + if (email && password) { + await this.logout(); + await this.env.session.login(email, password); + } else { + if (!this.env.flags.interactive) { + throw new FatalException( + 'Refusing to attempt browser login in non-interactive mode.\n' + + `If you are attempting to log in, make sure you are using a modern, interactive terminal. Otherwise, you can log in using inline username and password with ${input('ionic login ')}. See ${input('ionic login --help')} for more details.` + ); + } - if (sso) { - this.env.log.info( - `Ionic SSO Login\n` + - `During this process, a browser window will open to authenticate you with the identity provider for ${input(email)}. Please leave this process running until authentication is complete.` - ); + this.env.log.info(`During this process, a browser window will open to authenticate you. Please leave this process running until authentication is complete.`); this.env.log.nl(); - await this.env.session.ssoLogin(email); - } else { - await this.env.session.login(email, password); + const login = await this.env.prompt({ + type: 'confirm', + name: 'continue', + message: 'Open the browser to log in to your Ionic account?', + default: true, + }); + + if (login) { + await this.logout(); + await this.env.session.webLogin(); + } else { + return ; + } + } this.env.log.ok(success(strong('You are logged in!'))); } + + async logout() { + if (this.env.session.isLoggedIn()) { + await this.env.session.logout(); + this.env.config.set('tokens.telemetry', generateUUID()); + } + } } diff --git a/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts b/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts index b6dabcc46f..edfcd9474b 100644 --- a/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts +++ b/packages/@ionic/cli/src/commands/monitoring/syncmaps.ts @@ -45,7 +45,7 @@ By default, ${input('ionic monitoring syncmaps')} will upload the sourcemap file throw new FatalException(`Cannot run ${input('ionic monitoring syncmaps')} outside a project directory.`); } - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const appflowId = await this.project.requireAppflowId(); const [ snapshotId ] = inputs; diff --git a/packages/@ionic/cli/src/commands/package/build.ts b/packages/@ionic/cli/src/commands/package/build.ts index 529717ed1d..fe501949fa 100644 --- a/packages/@ionic/cli/src/commands/package/build.ts +++ b/packages/@ionic/cli/src/commands/package/build.ts @@ -213,7 +213,7 @@ This can be used only together with build type ${input('release')} for Android a throw new FatalException(`Cannot run ${input('ionic package build')} outside a project directory.`); } - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const appflowId = await this.project.requireAppflowId(); const [ platform, buildType ] = inputs; diff --git a/packages/@ionic/cli/src/commands/package/deploy.ts b/packages/@ionic/cli/src/commands/package/deploy.ts index 45a3db6bab..586c4cd2dd 100644 --- a/packages/@ionic/cli/src/commands/package/deploy.ts +++ b/packages/@ionic/cli/src/commands/package/deploy.ts @@ -115,7 +115,7 @@ Both can be retrieved from the Dashboard[^dashboard]. ); } - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const appflowId = await this.project.requireAppflowId(); const [buildId, destination] = inputs; diff --git a/packages/@ionic/cli/src/commands/ssh/add.ts b/packages/@ionic/cli/src/commands/ssh/add.ts index 1099461fd5..071e1b237e 100644 --- a/packages/@ionic/cli/src/commands/ssh/add.ts +++ b/packages/@ionic/cli/src/commands/ssh/add.ts @@ -79,7 +79,7 @@ export class SSHAddCommand extends SSHBaseCommand implements CommandPreRun { } const user = this.env.session.getUser(); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const sshkeyClient = new SSHKeyClient({ client: this.env.client, token, user }); try { diff --git a/packages/@ionic/cli/src/commands/ssh/delete.ts b/packages/@ionic/cli/src/commands/ssh/delete.ts index 9f99bd4a9b..39ef5b73ef 100644 --- a/packages/@ionic/cli/src/commands/ssh/delete.ts +++ b/packages/@ionic/cli/src/commands/ssh/delete.ts @@ -26,7 +26,7 @@ export class SSHDeleteCommand extends SSHBaseCommand implements CommandPreRun { if (!inputs[0]) { const user = this.env.session.getUser(); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); const paginator = sshkeyClient.paginate(); @@ -56,7 +56,7 @@ export class SSHDeleteCommand extends SSHBaseCommand implements CommandPreRun { const [ id ] = inputs; const user = this.env.session.getUser(); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); await sshkeyClient.delete(id); diff --git a/packages/@ionic/cli/src/commands/ssh/generate.ts b/packages/@ionic/cli/src/commands/ssh/generate.ts index 0829ca7d84..0ad3c45313 100644 --- a/packages/@ionic/cli/src/commands/ssh/generate.ts +++ b/packages/@ionic/cli/src/commands/ssh/generate.ts @@ -50,7 +50,7 @@ export class SSHGenerateCommand extends SSHBaseCommand implements CommandPreRun async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { await this.checkForOpenSSH(); - this.env.session.getUserToken(); + await this.env.session.getUserToken(); if (!options['annotation']) { options['annotation'] = this.env.config.get('user.email'); diff --git a/packages/@ionic/cli/src/commands/ssh/list.ts b/packages/@ionic/cli/src/commands/ssh/list.ts index bfb696d9ec..7583dbbb39 100644 --- a/packages/@ionic/cli/src/commands/ssh/list.ts +++ b/packages/@ionic/cli/src/commands/ssh/list.ts @@ -32,7 +32,7 @@ export class SSHListCommand extends SSHBaseCommand implements CommandPreRun { const { json } = options; const user = this.env.session.getUser(); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const sshkeyClient = new SSHKeyClient({ client: this.env.client, user, token }); const paginator = sshkeyClient.paginate(); diff --git a/packages/@ionic/cli/src/commands/start.ts b/packages/@ionic/cli/src/commands/start.ts index 31a18ab691..5d876c26ee 100644 --- a/packages/@ionic/cli/src/commands/start.ts +++ b/packages/@ionic/cli/src/commands/start.ts @@ -273,7 +273,7 @@ Use the ${input('--type')} option to start projects using older versions of Ioni if (!inputs[0]) { if (appflowId) { const { AppClient } = await import('../lib/app'); - const token = this.env.session.getUserToken(); + const token = await this.env.session.getUserToken(); const appClient = new AppClient(token, this.env); const tasks = this.createTaskChain(); tasks.next(`Looking up app ${input(appflowId)}`); diff --git a/packages/@ionic/cli/src/definitions.ts b/packages/@ionic/cli/src/definitions.ts index ec41f58e62..4b19d30827 100644 --- a/packages/@ionic/cli/src/definitions.ts +++ b/packages/@ionic/cli/src/definitions.ts @@ -233,6 +233,22 @@ export interface OAuthIdentityDetails { html_url: string; } +export interface OAuthServerConfig { + authorizationUrl: string; + tokenUrl: string; + clientId: string; + apiAudience: string; +} + +export interface OpenIdToken { + access_token: string; + expires_in: number; + id_token?: string; + refresh_token?: string; + scope: 'openid profile email offline_access'; + token_type: 'Bearer'; +} + export interface Snapshot { id: string; sha: string; @@ -269,6 +285,7 @@ export interface IConfig extends BaseConfig { getGitHost(): string; getGitPort(): number; getHTTPConfig(): CreateRequestOptions; + getOpenIDOAuthConfig(): OAuthServerConfig; } export interface ProjectPersonalizationDetails { @@ -387,11 +404,12 @@ export interface ISession { login(email: string, password: string): Promise; ssoLogin(email: string): Promise; tokenLogin(token: string): Promise; + webLogin(): Promise; logout(): Promise; isLoggedIn(): boolean; getUser(): { id: number; }; - getUserToken(): string; + getUserToken(): Promise; } export interface IShellSpawnOptions extends SpawnOptions { @@ -452,6 +470,16 @@ export interface ConfigFile { 'user.email'?: string; 'tokens.user'?: string; 'tokens.telemetry'?: string; + 'tokens.refresh'?: string; + 'tokens.issuedOn'?: string; + 'tokens.expiresInSeconds'?: number; + 'tokens.flowName'?: string; + + // oauth configs + 'oauth.openid.authorization_url'?: string; + 'oauth.openid.token_url'?: string; + 'oauth.openid.client_id'?: string; + 'oauth.openid.api_audience'?: string; // Features 'features.ssl-commands'?: boolean; @@ -509,10 +537,16 @@ export interface APIResponsePageTokenMeta extends APIResponseMeta { export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'PURGE' | 'HEAD' | 'OPTIONS'; +export const enum ContentType { + JSON = 'application/json', + FORM_URLENCODED = 'application/x-www-form-urlencoded', + HTML = 'text/html', +} + export interface IClient { config: IConfig; - make(method: HttpMethod, path: string): Promise<{ req: SuperAgentRequest; }>; + make(method: HttpMethod, path: string, contentType?: ContentType): Promise<{ req: SuperAgentRequest; }>; do(req: SuperAgentRequest): Promise; paginate>(args: PaginateArgs): IPaginator; } diff --git a/packages/@ionic/cli/src/guards.ts b/packages/@ionic/cli/src/guards.ts index 7bcaa4dcfd..09a79c3259 100644 --- a/packages/@ionic/cli/src/guards.ts +++ b/packages/@ionic/cli/src/guards.ts @@ -1,5 +1,35 @@ -import { APIResponse, APIResponseError, APIResponseSuccess, App, AppAssociation, BitbucketCloudRepoAssociation, BitbucketServerRepoAssociation, CommandPreRun, CordovaAndroidBuildOutputEntry, CordovaPackageJson, ExitCodeException, GithubBranch, GithubRepo, GithubRepoAssociation, ICommand, IMultiProjectConfig, IProjectConfig, IntegrationName, Login, Org, Response, SSHKey, SecurityProfile, Snapshot, StarterManifest, SuperAgentError, TreatableAilment, User } from './definitions'; -import { AuthConnection } from './lib/auth'; +import { + APIResponse, + APIResponseError, + APIResponseSuccess, + App, + AppAssociation, + BitbucketCloudRepoAssociation, + BitbucketServerRepoAssociation, + CommandPreRun, + CordovaAndroidBuildOutputEntry, + CordovaPackageJson, + ExitCodeException, + GithubBranch, + GithubRepo, + GithubRepoAssociation, + ICommand, + IMultiProjectConfig, + IProjectConfig, + IntegrationName, + Login, + OpenIdToken, + Org, + Response, + SSHKey, + SecurityProfile, + Snapshot, + StarterManifest, + SuperAgentError, + TreatableAilment, + User +} from './definitions'; +import { AuthConnection } from './lib/oauth/auth'; export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova', 'enterprise']; @@ -171,6 +201,20 @@ export function isOAuthLoginResponse(res: any): res is Response { return isAPIResponseSuccess(res) && isOAuthLogin(res.data); } +export function isOpenIDToken(tokenObj: any): tokenObj is OpenIdToken { + return tokenObj + && typeof tokenObj.access_token === 'string' + && typeof tokenObj.expires_in === 'number' + && (tokenObj.id_token ? typeof tokenObj.id_token === 'string' : true) + && (tokenObj.refresh_token ? typeof tokenObj.refresh_token === 'string' : true) + && tokenObj.scope === 'openid profile email offline_access' + && tokenObj.token_type === 'Bearer'; +} + +export function isOpenIDTokenExchangeResponse(res: any): res is Response { + return res && typeof res.body === 'object' && isOpenIDToken(res.body); +} + export function isSnapshot(snapshot: any): snapshot is Snapshot { return snapshot && typeof snapshot.id === 'string' diff --git a/packages/@ionic/cli/src/lib/config.ts b/packages/@ionic/cli/src/lib/config.ts index 1a926dd280..0e8464bf27 100644 --- a/packages/@ionic/cli/src/lib/config.ts +++ b/packages/@ionic/cli/src/lib/config.ts @@ -2,7 +2,7 @@ import { BaseConfig, BaseConfigOptions, MetadataGroup, ParsedArgs, metadataOptio import * as os from 'os'; import * as path from 'path'; -import { CommandMetadataOption, ConfigFile, CreateRequestOptions, IConfig } from '../definitions'; +import { CommandMetadataOption, ConfigFile, CreateRequestOptions, IConfig, OAuthServerConfig } from '../definitions'; export const GLOBAL_OPTIONS: readonly CommandMetadataOption[] = [ { @@ -116,6 +116,15 @@ export class Config extends BaseConfig implements IConfig { proxy: c['proxy'], }; } + + getOpenIDOAuthConfig(): OAuthServerConfig { + return { + authorizationUrl: this.get('oauth.openid.authorization_url', 'https://ionicframework.com/oauth/authorize'), + tokenUrl: this.get('oauth.openid.token_url', 'https://api.ionicjs.com/oauth/token'), + clientId: this.get('oauth.openid.client_id', 'cli'), + apiAudience: this.get('oauth.openid.api_audience', 'https://api.ionicjs.com'), + }; + } } export function parseGlobalOptions(pargv: string[]): ParsedArgs { diff --git a/packages/@ionic/cli/src/lib/doctor/ailments/index.ts b/packages/@ionic/cli/src/lib/doctor/ailments/index.ts index c9d6e0ed54..6addc087b5 100644 --- a/packages/@ionic/cli/src/lib/doctor/ailments/index.ts +++ b/packages/@ionic/cli/src/lib/doctor/ailments/index.ts @@ -157,7 +157,7 @@ export class GitConfigInvalid extends Ailment { return true; } - const token = this.session.getUserToken(); + const token = await this.session.getUserToken(); const appClient = new AppClient(token, { client: this.client }); const app = await appClient.load(appflowId); diff --git a/packages/@ionic/cli/src/lib/http.ts b/packages/@ionic/cli/src/lib/http.ts index 74327c0a4b..9f27646be1 100644 --- a/packages/@ionic/cli/src/lib/http.ts +++ b/packages/@ionic/cli/src/lib/http.ts @@ -2,7 +2,25 @@ import * as chalk from 'chalk'; import * as lodash from 'lodash'; import * as util from 'util'; -import { APIResponse, APIResponsePageTokenMeta, APIResponseSuccess, HttpMethod, IClient, IConfig, IPaginator, PagePaginatorState, PaginateArgs, PaginatorDeps, PaginatorGuard, PaginatorRequestGenerator, ResourceClientRequestModifiers, Response, SuperAgentError, TokenPaginatorState } from '../definitions'; +import { + APIResponse, + APIResponsePageTokenMeta, + APIResponseSuccess, + ContentType, + HttpMethod, + IClient, + IConfig, + IPaginator, + PagePaginatorState, + PaginateArgs, + PaginatorDeps, + PaginatorGuard, + PaginatorRequestGenerator, + ResourceClientRequestModifiers, + Response, + SuperAgentError, + TokenPaginatorState +} from '../definitions'; import { isAPIResponseError, isAPIResponseSuccess } from '../guards'; import { failure, strong } from './color'; @@ -13,7 +31,6 @@ export type SuperAgentRequest = import('superagent').SuperAgentRequest; export type SuperAgentResponse = import('superagent').Response; const FORMAT_ERROR_BODY_MAX_LENGTH = 1000; -export const CONTENT_TYPE_JSON = 'application/json'; export const ERROR_UNKNOWN_CONTENT_TYPE = 'UNKNOWN_CONTENT_TYPE'; export const ERROR_UNKNOWN_RESPONSE_FORMAT = 'UNKNOWN_RESPONSE_FORMAT'; @@ -21,13 +38,13 @@ export const ERROR_UNKNOWN_RESPONSE_FORMAT = 'UNKNOWN_RESPONSE_FORMAT'; export class Client implements IClient { constructor(public config: IConfig) {} - async make(method: HttpMethod, path: string): Promise<{ req: SuperAgentRequest; }> { + async make(method: HttpMethod, path: string, contentType: ContentType = ContentType.JSON): Promise<{ req: SuperAgentRequest; }> { const url = path.startsWith('http://') || path.startsWith('https://') ? path : `${this.config.getAPIUrl()}${path}`; const { req } = await createRequest(method, url, this.config.getHTTPConfig()); req - .set('Content-Type', CONTENT_TYPE_JSON) - .set('Accept', CONTENT_TYPE_JSON); + .set('Content-Type', contentType) + .set('Accept', ContentType.JSON); return { req }; } @@ -212,7 +229,7 @@ export function transformAPIResponse(r: SuperAgentResponse): APIResponse { r.body = { meta: { status: 204, version: '', request_id: '' } }; } - if (r.status !== 204 && r.type !== CONTENT_TYPE_JSON) { + if (r.status !== 204 && r.type !== ContentType.JSON) { throw ERROR_UNKNOWN_CONTENT_TYPE; } diff --git a/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts b/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts index 0f921c8f4a..b7be0e47ff 100644 --- a/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts +++ b/packages/@ionic/cli/src/lib/integrations/enterprise/index.ts @@ -115,7 +115,7 @@ export class Integration extends BaseIntegration { } protected async registerKey(key: ProductKey, appId: string) { - const token = this.e.session.getUserToken(); + const token = await this.e.session.getUserToken(); const { req } = await this.e.client.make('PATCH', `/orgs/${key.org.id}/keys/${key.id}`); req.set('Authorization', `Bearer ${token}`); req.send({ app_id: appId }); @@ -138,7 +138,7 @@ export class Integration extends BaseIntegration { protected async getAppClient() { const { AppClient } = await import('../../../lib/app'); - const token = this.e.session.getUserToken(); + const token = await this.e.session.getUserToken(); return new AppClient(token, this.e); } @@ -184,7 +184,7 @@ export class Integration extends BaseIntegration { } protected async getPK(pk: string): Promise { - const token = this.e.session.getUserToken(); + const token = await this.e.session.getUserToken(); const { req } = await this.e.client.make('GET', '/keys/self'); req.set('Authorization', `Bearer ${token}`).set('Product-Key-ID', pk); diff --git a/packages/@ionic/cli/src/lib/auth.ts b/packages/@ionic/cli/src/lib/oauth/auth.ts similarity index 81% rename from packages/@ionic/cli/src/lib/auth.ts rename to packages/@ionic/cli/src/lib/oauth/auth.ts index f437b657ae..6c09d496f6 100644 --- a/packages/@ionic/cli/src/lib/auth.ts +++ b/packages/@ionic/cli/src/lib/oauth/auth.ts @@ -1,7 +1,6 @@ -import { IClient, ResourceClientLoad } from '../definitions'; -import { isAuthConnectionResponse } from '../guards'; - -import { ResourceClient, createFatalAPIFormat } from './http'; +import { IClient, ResourceClientLoad } from '../../definitions'; +import { isAuthConnectionResponse } from '../../guards'; +import { ResourceClient, createFatalAPIFormat } from '../http'; export interface AuthConnection { readonly uuid: string; diff --git a/packages/@ionic/cli/src/lib/sso.ts b/packages/@ionic/cli/src/lib/oauth/oauth.ts similarity index 51% rename from packages/@ionic/cli/src/lib/sso.ts rename to packages/@ionic/cli/src/lib/oauth/oauth.ts index 4c79899548..e66bf57210 100644 --- a/packages/@ionic/cli/src/lib/sso.ts +++ b/packages/@ionic/cli/src/lib/oauth/oauth.ts @@ -4,11 +4,13 @@ import * as crypto from 'crypto'; import * as http from 'http'; import * as path from 'path'; import * as qs from 'querystring'; +import { Response } from 'superagent'; -import { ASSETS_DIRECTORY } from '../constants'; -import { IClient } from '../definitions'; - -import { openUrl } from './open'; +import { ASSETS_DIRECTORY } from '../../constants'; +import { ContentType, IClient, IConfig, OAuthServerConfig } from '../../definitions'; +import { FatalException } from '../errors'; +import { formatResponseError } from '../http'; +import { openUrl } from '../open'; const REDIRECT_PORT = 8123; const REDIRECT_HOST = 'localhost'; @@ -22,56 +24,81 @@ export interface TokenParameters { } export interface OAuth2FlowOptions { - readonly authorizationUrl: string; - readonly tokenUrl: string; - readonly clientId: string; readonly redirectHost?: string; readonly redirectPort?: number; + readonly accessTokenRequestContentType?: ContentType; } export interface OAuth2FlowDeps { readonly client: IClient; + readonly config: IConfig; } -export abstract class OAuth2Flow { - readonly authorizationUrl: string; - readonly tokenUrl: string; - readonly clientId: string; +export abstract class OAuth2Flow { + abstract readonly flowName: string; + readonly oauthConfig: OAuthServerConfig; readonly redirectHost: string; readonly redirectPort: number; + readonly accessTokenRequestContentType: ContentType; - constructor({ authorizationUrl, tokenUrl, clientId, redirectHost = REDIRECT_HOST, redirectPort = REDIRECT_PORT }: OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { - this.authorizationUrl = authorizationUrl; - this.tokenUrl = tokenUrl; - this.clientId = clientId; + constructor({ redirectHost = REDIRECT_HOST, redirectPort = REDIRECT_PORT, accessTokenRequestContentType = ContentType.JSON }: OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { + this.oauthConfig = this.getAuthConfig(); this.redirectHost = redirectHost; this.redirectPort = redirectPort; + this.accessTokenRequestContentType = accessTokenRequestContentType; } get redirectUrl(): string { return `http://${this.redirectHost}:${this.redirectPort}`; } - async run(): Promise { + async run(): Promise { const verifier = this.generateVerifier(); const challenge = this.generateChallenge(verifier); const authorizationParams = this.generateAuthorizationParameters(challenge); - const authorizationUrl = `${this.authorizationUrl}?${qs.stringify(authorizationParams)}`; + const authorizationUrl = `${this.oauthConfig.authorizationUrl}?${qs.stringify(authorizationParams)}`; await openUrl(authorizationUrl); const authorizationCode = await this.getAuthorizationCode(); - const token = await this.getAccessToken(authorizationCode, verifier); + const token = await this.exchangeAuthForAccessToken(authorizationCode, verifier); return token; } + async exchangeRefreshToken(refreshToken: string): Promise { + const params = this.generateRefreshTokenParameters(refreshToken); + const { req } = await this.e.client.make('POST', this.oauthConfig.tokenUrl, this.accessTokenRequestContentType); + + const res = await req.send(params); + + // check the response status code first here + if (!res.ok) { + throw new FatalException( + 'API request to refresh token was not successful.\n' + + 'Please try to login again.\n' + + formatResponseError(req, res.status) + ); + } + + if (!this.checkValidExchangeTokenRes(res)) { + throw new FatalException( + 'API request was successful, but the refreshed token was unrecognized.\n' + + 'Please try to login again.\n' + ); + } + return res.body; + } + protected abstract generateAuthorizationParameters(challenge: string): AuthorizationParameters; protected abstract generateTokenParameters(authorizationCode: string, verifier: string): TokenParameters; + protected abstract generateRefreshTokenParameters(refreshToken: string): TokenParameters; + protected abstract checkValidExchangeTokenRes(res: Response): boolean; + protected abstract getAuthConfig(): OAuthServerConfig; protected async getSuccessHtml(): Promise { - const p = path.resolve(ASSETS_DIRECTORY, 'sso', 'success', 'index.html'); + const p = path.resolve(ASSETS_DIRECTORY, 'oauth', 'success', 'index.html'); const contents = await readFile(p, { encoding: 'utf8' }); return contents; @@ -90,7 +117,7 @@ export abstract class OAuth2Flow { const params = qs.parse(req.url.substring(req.url.indexOf('?') + 1)); if (params.code) { - res.writeHead(200, { 'Content-Type': 'text/html' }); + res.writeHead(200, { 'Content-Type': ContentType.HTML }); res.end(successHtml); req.socket.destroy(); server.close(); @@ -106,13 +133,18 @@ export abstract class OAuth2Flow { }); } - protected async getAccessToken(authorizationCode: string, verifier: string): Promise { + protected async exchangeAuthForAccessToken(authorizationCode: string, verifier: string): Promise { const params = this.generateTokenParameters(authorizationCode, verifier); - const { req } = await this.e.client.make('POST', this.tokenUrl); + const { req } = await this.e.client.make('POST', this.oauthConfig.tokenUrl, this.accessTokenRequestContentType); const res = await req.send(params); - - return res.body.access_token; + if (!this.checkValidExchangeTokenRes(res)) { + throw new FatalException( + 'API request was successful, but the response format was unrecognized.\n' + + formatResponseError(req, res.status) + ); + } + return res.body; } protected generateVerifier(): string { @@ -130,50 +162,3 @@ export abstract class OAuth2Flow { .replace(/=/g, ''); } } - -const AUTHORIZATION_URL = 'https://auth.ionicframework.com/authorize'; -const TOKEN_URL = 'https://auth.ionicframework.com/oauth/token'; -const CLIENT_ID = '0kTF4wm74vppjImr11peCjQo2PIQDS3m'; -const API_AUDIENCE = 'https://api.ionicjs.com'; - -export interface Auth0OAuth2FlowOptions extends Partial { - readonly email: string; - readonly connection: string; - readonly audience?: string; -} - -export class Auth0OAuth2Flow extends OAuth2Flow { - readonly email: string; - readonly audience: string; - readonly connection: string; - - constructor({ email, connection, audience = API_AUDIENCE, authorizationUrl = AUTHORIZATION_URL, tokenUrl = TOKEN_URL, clientId = CLIENT_ID, ...options }: Auth0OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { - super({ authorizationUrl, tokenUrl, clientId, ...options }, e); - this.email = email; - this.connection = connection; - this.audience = audience; - } - - protected generateAuthorizationParameters(challenge: string): AuthorizationParameters { - return { - audience: this.audience, - scope: 'openid profile email offline_access', - response_type: 'code', - connection: this.connection, - client_id: this.clientId, - code_challenge: challenge, - code_challenge_method: 'S256', - redirect_uri: this.redirectUrl, - }; - } - - protected generateTokenParameters(code: string, verifier: string): TokenParameters { - return { - grant_type: 'authorization_code', - client_id: this.clientId, - code_verifier: verifier, - code, - redirect_uri: this.redirectUrl, - }; - } -} diff --git a/packages/@ionic/cli/src/lib/oauth/openid.ts b/packages/@ionic/cli/src/lib/oauth/openid.ts new file mode 100644 index 0000000000..262e0e34c9 --- /dev/null +++ b/packages/@ionic/cli/src/lib/oauth/openid.ts @@ -0,0 +1,64 @@ +import { Response } from 'superagent'; + +import { ContentType, OAuthServerConfig, OpenIdToken } from '../../definitions'; +import { isOpenIDTokenExchangeResponse } from '../../guards'; + +import { + AuthorizationParameters, + OAuth2Flow, + OAuth2FlowDeps, + OAuth2FlowOptions, + TokenParameters +} from './oauth'; + +export interface OpenIDFlowOptions extends Partial { + readonly accessTokenRequestContentType?: ContentType; +} + +export class OpenIDFlow extends OAuth2Flow { + readonly flowName = 'open_id'; + + constructor({ accessTokenRequestContentType = ContentType.FORM_URLENCODED, ...options }: OpenIDFlowOptions, readonly e: OAuth2FlowDeps) { + super({ accessTokenRequestContentType, ...options }, e); + } + + protected generateAuthorizationParameters(challenge: string): AuthorizationParameters { + return { + audience: this.oauthConfig.apiAudience, + scope: 'openid profile email offline_access', + response_type: 'code', + client_id: this.oauthConfig.clientId, + code_challenge: challenge, + code_challenge_method: 'S256', + redirect_uri: this.redirectUrl, + nonce: this.generateVerifier(), + }; + } + + protected generateTokenParameters(code: string, verifier: string): TokenParameters { + return { + grant_type: 'authorization_code', + client_id: this.oauthConfig.clientId, + code_verifier: verifier, + code, + redirect_uri: this.redirectUrl, + }; + } + + protected generateRefreshTokenParameters(refreshToken: string): TokenParameters { + return { + refresh_token: refreshToken, + grant_type: 'refresh_token', + client_id: this.oauthConfig.clientId, + }; + } + + protected checkValidExchangeTokenRes(res: Response): boolean { + return isOpenIDTokenExchangeResponse(res); + } + + protected getAuthConfig(): OAuthServerConfig { + return this.e.config.getOpenIDOAuthConfig(); + } + +} diff --git a/packages/@ionic/cli/src/lib/session.ts b/packages/@ionic/cli/src/lib/session.ts index b6f3db3ffd..83d3c16bf4 100644 --- a/packages/@ionic/cli/src/lib/session.ts +++ b/packages/@ionic/cli/src/lib/session.ts @@ -1,5 +1,3 @@ -import { combine } from '@ionic/cli-framework'; - import { IClient, IConfig, ISession, IonicEnvironment } from '../definitions'; import { isLoginResponse, isSuperAgentError } from '../guards'; @@ -21,7 +19,12 @@ export class BaseSession { this.e.config.unset('user.id'); this.e.config.unset('user.email'); this.e.config.unset('tokens.user'); + this.e.config.unset('tokens.refresh'); + this.e.config.unset('tokens.expiresInSeconds'); + this.e.config.unset('tokens.issuedOn'); + this.e.config.unset('tokens.flowName'); this.e.config.set('git.setup', false); + } isLoggedIn(): boolean { @@ -40,9 +43,12 @@ export class BaseSession { return { id: userId }; } +} + +export class ProSession extends BaseSession implements ISession { - getUserToken(): string { - const userToken = this.e.config.get('tokens.user'); + async getUserToken(): Promise { + let userToken = this.e.config.get('tokens.user'); if (!userToken) { throw new SessionException( @@ -51,11 +57,30 @@ export class BaseSession { ); } + const tokenIssuedOn = this.e.config.get('tokens.issuedOn'); + const tokenExpirationSeconds = this.e.config.get('tokens.expiresInSeconds'); + const refreshToken = this.e.config.get('tokens.refresh'); + const flowName = this.e.config.get('tokens.flowName'); + + // if there is the possibility to refresh the token, try to do it + if (tokenIssuedOn && tokenExpirationSeconds && refreshToken && flowName) { + if (!this.isTokenValid(tokenIssuedOn, tokenExpirationSeconds)) { + userToken = await this.refreshLogin(refreshToken, flowName); + } + } + + // otherwise simply return the token return userToken; } -} -export class ProSession extends BaseSession implements ISession { + private isTokenValid(tokenIssuedOn: string, tokenExpirationSeconds: number): boolean { + const tokenExpirationMilliSeconds = tokenExpirationSeconds * 1000; + // 15 minutes in milliseconds of margin + const marginExpiration = 15 * 60 * 1000; + const tokenValid = new Date() < new Date(new Date(tokenIssuedOn).getTime() + tokenExpirationMilliSeconds - marginExpiration); + return tokenValid; + } + async login(email: string, password: string): Promise { const { req } = await this.e.client.make('POST', '/login'); req.send({ email, password, source: 'cli' }); @@ -94,19 +119,8 @@ export class ProSession extends BaseSession implements ISession { } } - async ssoLogin(email: string): Promise { - const { AuthClient } = await import('./auth'); - const { Auth0OAuth2Flow } = await import('./sso'); - - const authClient = new AuthClient(this.e); - const { uuid: connection } = await authClient.connections.load(email); - - const flow = new Auth0OAuth2Flow({ audience: this.e.config.get('urls.api'), email, connection }, this.e); - const token = await flow.run(); - - await this.tokenLogin(token); - - this.e.config.set('org.id', connection); + async ssoLogin(email?: string): Promise { + await this.webLogin(); } async tokenLogin(token: string): Promise { @@ -133,33 +147,61 @@ export class ProSession extends BaseSession implements ISession { throw e; } } + + async webLogin(): Promise { + const { OpenIDFlow } = await import('./oauth/openid'); + const flow = new OpenIDFlow({}, this.e); + const token = await flow.run(); + + await this.tokenLogin(token.access_token); + + this.e.config.set('tokens.refresh', token.refresh_token); + this.e.config.set('tokens.expiresInSeconds', token.expires_in); + this.e.config.set('tokens.issuedOn', (new Date()).toJSON()); + this.e.config.set('tokens.flowName', flow.flowName); + } + + async refreshLogin(refreshToken: string, flowName: string): Promise { + let oauthflow; + // having a generic way to access the right refresh token flow + switch (flowName) { + case 'open_id': + const { OpenIDFlow } = await import('./oauth/openid'); + oauthflow = new OpenIDFlow({}, this.e); + break; + default: + oauthflow = undefined; + } + if (!oauthflow) { + throw new FatalException('Token cannot be refreshed'); + } + + const token = await oauthflow.exchangeRefreshToken(refreshToken); + await this.tokenLogin(token.access_token); + this.e.config.set('tokens.expiresInSeconds', token.expires_in); + this.e.config.set('tokens.issuedOn', (new Date()).toJSON()); + + return token.access_token; + } } export async function promptToLogin(env: IonicEnvironment): Promise { - const { validators } = await import('@ionic/cli-framework'); - env.log.nl(); env.log.msg( `Log in to your Ionic account!\n` + `If you don't have one yet, create yours by running: ${input(`ionic signup`)}\n` ); - const email = await env.prompt({ - type: 'input', - name: 'email', - message: 'Email:', - validate: v => combine(validators.required, validators.email)(v), - }); - - const password = await env.prompt({ - type: 'password', - name: 'password', - message: 'Password:', - mask: '*', - validate: v => validators.required(v), + const login = await env.prompt({ + type: 'confirm', + name: 'login', + message: 'Open the browser to log in to your Ionic account?', + default: true, }); - await env.session.login(email, password); + if (login) { + await env.session.webLogin(); + } } export async function promptToSignup(env: IonicEnvironment): Promise { diff --git a/packages/@ionic/cli/src/lib/telemetry.ts b/packages/@ionic/cli/src/lib/telemetry.ts index 07d0f5f186..62a813b09b 100644 --- a/packages/@ionic/cli/src/lib/telemetry.ts +++ b/packages/@ionic/cli/src/lib/telemetry.ts @@ -104,7 +104,7 @@ export async function sendCommand({ config, client, getInfo, ctx, session, proje const isLoggedIn = session.isLoggedIn(); if (isLoggedIn) { - const token = session.getUserToken(); + const token = await session.getUserToken(); req.set('Authorization', `Bearer ${token}`); } From e3b99adaec35ae149976d2ffe7897bbec3c501af Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 14:05:23 -0700 Subject: [PATCH 13/15] chore(deps-dev): bump @types/uuid from 7.0.4 to 8.0.0 (#4449) Bumps [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid) from 7.0.4 to 8.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@ionic/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index c401a9bc9f..13cb8b1f08 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -83,7 +83,7 @@ "@types/superagent-proxy": "^2.0.0", "@types/tar": "^4.0.0", "@types/through2": "^2.0.33", - "@types/uuid": "^7.0.0", + "@types/uuid": "^8.0.0", "jest": "^25.1.0", "jest-cli": "^26.0.1", "lint-staged": "^10.0.2", From f8e0902908aebe1263fc073148428f805407a7a3 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Tue, 2 Jun 2020 14:05:52 -0700 Subject: [PATCH 14/15] chore(ci): CD workflow --- .github/workflows/cd.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000000..993901c55b --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,34 @@ +name: CD + +on: + push: + branches: + - master + +jobs: + build: + name: Build, Test, and Deploy + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + - uses: actions/setup-node@v1 + with: + node-version: 14.x + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Restore Dependency Cache + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.OS }}-dependency-cache-${{ hashFiles('**/package.json') }} + - run: npm install + - run: npm run bootstrap + - run: npm run publish:ci -- --yes + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + GIT_AUTHOR_NAME: Ionitron + GIT_AUTHOR_EMAIL: hi@ionicframework.com + GIT_COMMITTER_NAME: Ionitron + GIT_COMMITTER_EMAIL: hi@ionicframework.com From 483d9ab593c4715ab21b3b721c6735c7e01d367e Mon Sep 17 00:00:00 2001 From: Ionitron Date: Tue, 2 Jun 2020 21:17:07 +0000 Subject: [PATCH 15/15] chore(release): publish [skip ci] - @ionic/cli-framework@4.2.0 - @ionic/cli@6.10.0 - @ionic/lab@3.1.7 - @ionic/v1-toolkit@3.2.3 - cli-scripts@2.1.17 --- packages/@ionic/cli-framework/CHANGELOG.md | 11 +++++++++++ packages/@ionic/cli-framework/package.json | 2 +- packages/@ionic/cli/CHANGELOG.md | 16 ++++++++++++++++ packages/@ionic/cli/package.json | 4 ++-- packages/@ionic/lab/CHANGELOG.md | 8 ++++++++ packages/@ionic/lab/package.json | 4 ++-- packages/@ionic/v1-toolkit/CHANGELOG.md | 8 ++++++++ packages/@ionic/v1-toolkit/package.json | 4 ++-- packages/cli-scripts/CHANGELOG.md | 8 ++++++++ packages/cli-scripts/package.json | 6 +++--- 10 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/@ionic/cli-framework/CHANGELOG.md b/packages/@ionic/cli-framework/CHANGELOG.md index dae024ae9b..48aa76ab19 100644 --- a/packages/@ionic/cli-framework/CHANGELOG.md +++ b/packages/@ionic/cli-framework/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.5...@ionic/cli-framework@4.2.0) (2020-06-02) + + +### Features + +* **help:** formatBeforeInputSummary and formatAfterInputSummary ([70ac3ab](https://github.com/ionic-team/ionic-cli/commit/70ac3ab633dc7dd65a2ac344c81a01f7914597b8)) + + + + + ## [4.1.5](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@4.1.4...@ionic/cli-framework@4.1.5) (2020-05-12) diff --git a/packages/@ionic/cli-framework/package.json b/packages/@ionic/cli-framework/package.json index 6b3b7369b8..664e72bccc 100644 --- a/packages/@ionic/cli-framework/package.json +++ b/packages/@ionic/cli-framework/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/cli-framework", - "version": "4.1.5", + "version": "4.2.0", "description": "The foundation framework of the Ionic CLI", "homepage": "https://ionicframework.com/", "author": "Ionic Team (https://ionicframework.com) ", diff --git a/packages/@ionic/cli/CHANGELOG.md b/packages/@ionic/cli/CHANGELOG.md index 594685d3fc..e124956fea 100644 --- a/packages/@ionic/cli/CHANGELOG.md +++ b/packages/@ionic/cli/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.10.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.3...@ionic/cli@6.10.0) (2020-06-02) + + +### Bug Fixes + +* **cordova:** platform is a required input ([1bd3087](https://github.com/ionic-team/ionic-cli/commit/1bd30874f46d89248193c959dd8d769dd99503cf)) + + +### Features + +* **login:** default to browser login ([#4451](https://github.com/ionic-team/ionic-cli/issues/4451)) ([0738abd](https://github.com/ionic-team/ionic-cli/commit/0738abd4123b6b5e078cd7ff2f76ed1a26474cf0)) + + + + + ## [6.9.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli@6.9.2...@ionic/cli@6.9.3) (2020-05-27) diff --git a/packages/@ionic/cli/package.json b/packages/@ionic/cli/package.json index 13cb8b1f08..110bebdbef 100644 --- a/packages/@ionic/cli/package.json +++ b/packages/@ionic/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/cli", - "version": "6.9.3", + "version": "6.10.0", "description": "A tool for creating and developing Ionic Framework mobile apps.", "homepage": "https://ionicframework.com", "author": "Ionic Team (https://ionicframework.com) ", @@ -40,7 +40,7 @@ }, "license": "MIT", "dependencies": { - "@ionic/cli-framework": "4.1.5", + "@ionic/cli-framework": "4.2.0", "@ionic/cli-framework-prompts": "2.1.3", "@ionic/utils-array": "2.1.3", "@ionic/utils-fs": "3.1.3", diff --git a/packages/@ionic/lab/CHANGELOG.md b/packages/@ionic/lab/CHANGELOG.md index d0f028ce31..47bec03f11 100644 --- a/packages/@ionic/lab/CHANGELOG.md +++ b/packages/@ionic/lab/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.1.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/lab@3.1.6...@ionic/lab@3.1.7) (2020-06-02) + +**Note:** Version bump only for package @ionic/lab + + + + + ## [3.1.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/lab@3.1.5...@ionic/lab@3.1.6) (2020-05-12) diff --git a/packages/@ionic/lab/package.json b/packages/@ionic/lab/package.json index dd40e32110..27101a3530 100644 --- a/packages/@ionic/lab/package.json +++ b/packages/@ionic/lab/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/lab", - "version": "3.1.6", + "version": "3.1.7", "description": "Ionic Lab utility for developing Ionic apps, used by Ionic CLI", "bin": { "ionic-lab": "./bin/ionic-lab" @@ -41,7 +41,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@ionic/cli-framework": "4.1.5", + "@ionic/cli-framework": "4.2.0", "@ionic/utils-fs": "3.1.3", "chalk": "^4.0.0", "express": "^4.16.2", diff --git a/packages/@ionic/v1-toolkit/CHANGELOG.md b/packages/@ionic/v1-toolkit/CHANGELOG.md index e0aa688b47..a0bb29f353 100644 --- a/packages/@ionic/v1-toolkit/CHANGELOG.md +++ b/packages/@ionic/v1-toolkit/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.2.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/v1-toolkit@3.2.2...@ionic/v1-toolkit@3.2.3) (2020-06-02) + +**Note:** Version bump only for package @ionic/v1-toolkit + + + + + ## [3.2.2](https://github.com/ionic-team/ionic-cli/compare/@ionic/v1-toolkit@3.2.1...@ionic/v1-toolkit@3.2.2) (2020-05-12) diff --git a/packages/@ionic/v1-toolkit/package.json b/packages/@ionic/v1-toolkit/package.json index ff6767042d..c4e004ed71 100644 --- a/packages/@ionic/v1-toolkit/package.json +++ b/packages/@ionic/v1-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/v1-toolkit", - "version": "3.2.2", + "version": "3.2.3", "description": "Utility CLI for Ionic 1.x apps, used by Ionic CLI", "bin": { "ionic-v1": "./bin/ionic-v1" @@ -36,7 +36,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@ionic/cli-framework": "4.1.5", + "@ionic/cli-framework": "4.2.0", "@ionic/utils-fs": "3.1.3", "chalk": "^4.0.0", "chokidar": "^3.0.1", diff --git a/packages/cli-scripts/CHANGELOG.md b/packages/cli-scripts/CHANGELOG.md index add4d07b90..c96962c317 100644 --- a/packages/cli-scripts/CHANGELOG.md +++ b/packages/cli-scripts/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.1.17](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.16...cli-scripts@2.1.17) (2020-06-02) + +**Note:** Version bump only for package cli-scripts + + + + + ## [2.1.16](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@2.1.15...cli-scripts@2.1.16) (2020-05-27) **Note:** Version bump only for package cli-scripts diff --git a/packages/cli-scripts/package.json b/packages/cli-scripts/package.json index 126b6a991a..01de2a8072 100644 --- a/packages/cli-scripts/package.json +++ b/packages/cli-scripts/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "cli-scripts", - "version": "2.1.16", + "version": "2.1.17", "bin": { "ionic-cli-scripts": "./bin/ionic-cli-scripts" }, @@ -16,8 +16,8 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@ionic/cli": "6.9.3", - "@ionic/cli-framework": "4.1.5", + "@ionic/cli": "6.10.0", + "@ionic/cli-framework": "4.2.0", "@ionic/utils-fs": "3.1.3", "ansi-styles": "^4.0.0", "chalk": "^4.0.0",