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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/ionic/assets/sso/success/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Success</title>
<style>
@font-face {font-family: 'AvenirNextLTPro-Regular';src: url('https://code.ionicframework.com/assets/fonts/28882F_0_0.eot');src: url('https://code.ionicframework.com/assets/fonts/28882F_0_0.eot?#iefix') format('embedded-opentype'),url('https://code.ionicframework.com/assets/fonts/28882F_0_0.woff') format('woff'),url('https://code.ionicframework.com/assets/fonts/28882F_0_0.ttf') format('truetype');}
@font-face {font-family: 'AvenirNextLTPro-Medium';src: url('https://code.ionicframework.com/assets/fonts/28882F_1_0.eot');src: url('https://code.ionicframework.com/assets/fonts/28882F_1_0.eot?#iefix') format('embedded-opentype'),url('https://code.ionicframework.com/assets/fonts/28882F_1_0.woff') format('woff'),url('https://code.ionicframework.com/assets/fonts/28882F_1_0.ttf') format('truetype');}
@font-face {font-family: 'AvenirNextLTPro-UltLt';src: url('https://code.ionicframework.com/assets/fonts/29CC36_0_0.eot');src: url('https://code.ionicframework.com/assets/fonts/29CC36_0_0.eot?#iefix') format('embedded-opentype'),url('https://code.ionicframework.com/assets/fonts/29CC36_0_0.woff') format('woff'),url('https://code.ionicframework.com/assets/fonts/29CC36_0_0.ttf') format('truetype');}

*, *::before, *::after {
box-sizing: border-box;
}

html, body {
margin: 0;
padding: 0;
height: 100%;
}

body {
display: flex;
background-color: #242a31;
color: #a2a9b4;
font-family: 'AvenirNextLTPro-Regular', 'Helvetica Neue', 'Helvetica', Arial, sans-serif;
align-items: center;
justify-content: center;
}

div {
background-color: #151a21;
text-align: center;
padding: 2em;
}

h1, p {
margin: 0;
}

p {
margin-top: 1em;
}
</style>
</head>
<body>
<div>
<h1>You are authenticated.</h1>
<p>Please return to your terminal. You may close this window.</p>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/ionic/src/commands/git/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://dashboard.ionicframework.com')}

const token = this.env.session.getUserToken();
const proId = await this.project.requireProId();
const appClient = new AppClient({ token, client: this.env.client });
const appClient = new AppClient(token, this.env);
const app = await appClient.load(proId);

if (!app.repo_url) {
Expand Down
7 changes: 4 additions & 3 deletions packages/ionic/src/commands/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request')
private async getAppClient() {
const { AppClient } = await import('../lib/app');
const token = this.env.session.getUserToken();
return new AppClient({ token, client: this.env.client });
return new AppClient(token, this.env);
}

private async getUserClient() {
const { UserClient } = await import('../lib/user');
const token = this.env.session.getUserToken();
return new UserClient({ token, client: this.env.client });
return new UserClient(token, this.env);
}

async lookUpApp(proId: string): Promise<App> {
Expand All @@ -243,7 +243,8 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request')

async createApp({ name }: { name: string; }, runinfo: CommandInstanceInfo): Promise<string> {
const appClient = await this.getAppClient();
const app = await appClient.create({ name });
const org_id = this.env.config.get('org.id');
const app = await appClient.create({ name, org_id });

await this.linkApp(app, runinfo);

Expand Down
64 changes: 48 additions & 16 deletions packages/ionic/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { validators } from '@ionic/cli-framework';
import { OptionGroup, validators } from '@ionic/cli-framework';
import chalk from 'chalk';

import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions';
import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions';
import { Command } from '../lib/command';
import { FatalException } from '../lib/errors';
import { runCommand } from '../lib/executor';
import { generateUUID } from '../lib/utils/uuid';

export class LoginCommand extends Command implements CommandPreRun {
Expand Down Expand Up @@ -37,35 +36,58 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
{
name: 'password',
summary: 'Your password',
validators: [validators.required],
// 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: [OptionGroup.Hidden],
},
],
};
}

async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
const sso = !!options['sso'];

if (options['email'] || options['password']) {
throw new FatalException(
`${chalk.green('email')} and ${chalk.green('password')} are command arguments, not options. Please try this:\n` +
`${chalk.green('ionic login <email> <password>')}\n`
);
}

const askForEmail = !inputs[0];
const askForPassword = !sso && !inputs[1];

if (this.env.session.isLoggedIn()) {
const extra = !inputs[0] || !inputs[1] ? 'Prompting for new credentials.' : 'Attempting login.';
const email = this.env.config.get('user.email');
this.env.log.warn(`You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${this.env.flags.interactive ? extra : ''}`);

const extra = askForEmail || 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.';

this.env.log.warn(
'You will be logged out.\n' +
`You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${extra}`
);
this.env.log.nl();
} else {
this.env.log.msg(
`Log into your Ionic Pro account\n` +
`If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}\n`
this.env.log.info(
`Log into your Ionic Pro account!\n` +
`If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}`
);
this.env.log.nl();
}

// TODO: combine with promptToLogin ?

if (!inputs[0]) {
if (askForEmail) {
const email = await this.env.prompt({
type: 'input',
name: 'email',
Expand All @@ -76,7 +98,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
inputs[0] = email;
}

if (!inputs[1]) {
if (askForPassword) {
const password = await this.env.prompt({
type: 'password',
name: 'password',
Expand All @@ -89,17 +111,27 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
}
}

async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise<void> {
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
const [ email, password ] = inputs;
const sso = !!options['sso'];

if (this.env.session.isLoggedIn()) {
this.env.log.msg('Logging you out.');
await runCommand(runinfo, ['logout']);
await this.env.session.logout();
this.env.config.set('tokens.telemetry', generateUUID());
}

await this.env.session.login(email, password);
if (sso) {
this.env.log.info(
`Ionic Pro SSO Login\n` +
`During this process, a browser window will open to authenticate you with the identity provider for ${chalk.green(email)}. 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);
}

this.env.log.ok('You are logged in!');
this.env.log.ok(chalk.green.bold('You are logged in!'));
}
}
2 changes: 1 addition & 1 deletion packages/ionic/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
if (proId) {
const { AppClient } = await import('../lib/app');
const token = this.env.session.getUserToken();
const appClient = new AppClient({ token, client: this.env.client });
const appClient = new AppClient(token, this.env);
const tasks = this.createTaskChain();
tasks.next(`Looking up app ${chalk.green(proId)}`);
const app = await appClient.load(proId);
Expand Down
2 changes: 2 additions & 0 deletions packages/ionic/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ export type NamespaceLocateResult = ζframework.NamespaceLocateResult<ICommand,

export interface ISession {
login(email: string, password: string): Promise<void>;
ssoLogin(email: string): Promise<void>;
tokenLogin(token: string): Promise<void>;
logout(): Promise<void>;

Expand Down Expand Up @@ -385,6 +386,7 @@ export interface ConfigFile {
'git.host'?: string;
'git.port'?: number;
'git.setup'?: boolean;
'org.id'?: string;
'user.id'?: number;
'user.email'?: string;
'tokens.user'?: string;
Expand Down
10 changes: 10 additions & 0 deletions packages/ionic/src/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
User,
} from './definitions';

import { AuthConnection } from './lib/auth';

export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova'];

export function isCommand(cmd: any): cmd is ICommand {
Expand Down Expand Up @@ -219,6 +221,14 @@ export function isLoginResponse(res: APIResponse): res is Response<Login> {
return isAPIResponseSuccess(res) && isLogin(res.data);
}

export function isAuthConnection(connection: any): connection is AuthConnection {
return connection && typeof connection.uuid === 'string';
}

export function isAuthConnectionResponse(res: APIResponse): res is Response<AuthConnection> {
return isAPIResponseSuccess(res) && isAuthConnection(res.data);
}

export function isUser(user: any): user is User {
return user
&& typeof user.id === 'number'
Expand Down
43 changes: 19 additions & 24 deletions packages/ionic/src/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,22 @@ export function formatName(app: Pick<App, 'name' | 'org'>) {

export interface AppClientDeps {
readonly client: IClient;
readonly token: string;
}

export interface AppCreateDetails {
name: string;
readonly name: string;
readonly org_id?: string;
}

export class AppClient extends ResourceClient implements ResourceClientLoad<App>, ResourceClientCreate<App, AppCreateDetails>, ResourceClientPaginate<App> {
protected client: IClient;
protected token: string;

constructor({ client, token }: AppClientDeps) {
constructor(readonly token: string, readonly e: AppClientDeps) {
super();
this.client = client;
this.token = token;
}

async load(id: string): Promise<App> {
const { req } = await this.client.make('GET', `/apps/${id}`);
const { req } = await this.e.client.make('GET', `/apps/${id}`);
this.applyAuthentication(req, this.token);
const res = await this.client.do(req);
const res = await this.e.client.do(req);

if (!isAppResponse(res)) {
throw createFatalAPIFormat(req, res);
Expand All @@ -44,11 +39,11 @@ export class AppClient extends ResourceClient implements ResourceClientLoad<App>
return res.data;
}

async create({ name }: AppCreateDetails): Promise<App> {
const { req } = await this.client.make('POST', '/apps');
async create(details: AppCreateDetails): Promise<App> {
const { req } = await this.e.client.make('POST', '/apps');
this.applyAuthentication(req, this.token);
req.send({ name });
const res = await this.client.do(req);
req.send(details);
const res = await this.e.client.do(req);

if (!isAppResponse(res)) {
throw createFatalAPIFormat(req, res);
Expand All @@ -58,9 +53,9 @@ export class AppClient extends ResourceClient implements ResourceClientLoad<App>
}

paginate(args: Partial<PaginateArgs<Response<App[]>>> = {}): IPaginator<Response<App[]>, PaginatorState> {
return this.client.paginate({
return this.e.client.paginate({
reqgen: async () => {
const { req } = await this.client.make('GET', '/apps');
const { req } = await this.e.client.make('GET', '/apps');
this.applyAuthentication(req, this.token);
return { req };
},
Expand All @@ -70,17 +65,17 @@ export class AppClient extends ResourceClient implements ResourceClientLoad<App>
}

async createAssociation(id: string, association: { repoId: number; type: AssociationType; branches: string[] }): Promise<AppAssociation> {
const { req } = await this.client.make('POST', `/apps/${id}/repository`);
const { req } = await this.e.client.make('POST', `/apps/${id}/repository`);

req
.set('Authorization', `Bearer ${this.token}`)
.send({
repository_id: association.repoId,
type: association.type,
branches: association.branches,
});
.send({
repository_id: association.repoId,
type: association.type,
branches: association.branches,
});

const res = await this.client.do(req);
const res = await this.e.client.do(req);

if (!isAppAssociationResponse(res)) {
throw createFatalAPIFormat(req, res);
Expand All @@ -90,7 +85,7 @@ export class AppClient extends ResourceClient implements ResourceClientLoad<App>
}

async deleteAssociation(id: string): Promise<void> {
const { req } = await this.client.make('DELETE', `/apps/${id}/repository`);
const { req } = await this.e.client.make('DELETE', `/apps/${id}/repository`);

req
.set('Authorization', `Bearer ${this.token}`)
Expand Down
38 changes: 38 additions & 0 deletions packages/ionic/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IClient, ResourceClientLoad } from '../definitions';
import { isAuthConnectionResponse } from '../guards';

import { ResourceClient, createFatalAPIFormat } from './http';

export interface AuthConnection {
readonly uuid: string;
}

export interface AuthClientDeps {
readonly client: IClient;
}

export class AuthClient extends ResourceClient {
readonly connections: AuthConnectionClient;

constructor(readonly e: AuthClientDeps) {
super();
this.connections = new AuthConnectionClient(e);
}
}

export class AuthConnectionClient extends ResourceClient implements ResourceClientLoad<AuthConnection> {
constructor(readonly e: AuthClientDeps) {
super();
}

async load(email: string): Promise<AuthConnection> {
const { req } = await this.e.client.make('GET', `/auth/connections/${email}`);
const res = await this.e.client.do(req);

if (!isAuthConnectionResponse(res)) {
throw createFatalAPIFormat(req, res);
}

return res.data;
}
}
2 changes: 1 addition & 1 deletion packages/ionic/src/lib/doctor/ailments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class GitConfigInvalid extends Ailment {
}

const token = this.session.getUserToken();
const appClient = new AppClient({ token, client: this.client });
const appClient = new AppClient(token, { client: this.client });
const app = await appClient.load(proId);

if (app.repo_url !== remote) {
Expand Down
Loading