Skip to content

Commit

Permalink
feat: added login/logout (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx authored May 6, 2018
1 parent a9a52f0 commit c772322
Show file tree
Hide file tree
Showing 7 changed files with 499 additions and 218 deletions.
11 changes: 11 additions & 0 deletions examples/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Command} from '../src'

class LoginCommand extends Command {
async run() {
this.log('logging in')
await this.heroku.login()
}
}

LoginCommand.run([])
.catch(require('@oclif/errors/handle'))
11 changes: 11 additions & 0 deletions examples/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Command} from '../src'

class LoginCommand extends Command {
async run() {
this.log('logging out')
await this.heroku.logout()
}
}

LoginCommand.run([])
.catch(require('@oclif/errors/handle'))
30 changes: 17 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,39 @@
"author": "Jeff Dickey @jdxcode",
"bugs": "https://github.com/heroku/heroku-cli-command/issues",
"dependencies": {
"@oclif/errors": "^1.0.6",
"cli-ux": "^3.4.1",
"@heroku-cli/color": "^1.1.3",
"@oclif/errors": "^1.0.9",
"cli-ux": "^4.0.0",
"debug": "^3.1.0",
"heroku-client": "3.0.6",
"http-call": "^5.1.1",
"netrc-parser": "^3.1.4"
"http-call": "^5.1.2",
"netrc-parser": "^3.1.4",
"opn": "^5.3.0"
},
"devDependencies": {
"@heroku-cli/tslint": "^1.1.4",
"@oclif/command": "^1.4.17",
"@oclif/config": "^1.6.16",
"@oclif/tslint": "^1.1.0",
"@oclif/command": "^1.4.21",
"@oclif/config": "^1.6.17",
"@oclif/tslint": "^1.1.1",
"@types/ansi-styles": "^2.0.30",
"@types/chai": "^4.1.3",
"@types/mocha": "^5.2.0",
"@types/nock": "9.1.3",
"@types/node": "^8.10.11",
"@types/node": "^10.0.4",
"@types/opn": "^5.1.0",
"@types/proxyquire": "^1.3.28",
"@types/sinon": "^4.3.1",
"@types/supports-color": "^5.3.0",
"chai": "^4.1.2",
"fancy-test": "^1.0.5",
"fancy-test": "^1.0.6",
"mocha": "^5.1.1",
"nock": "9.2.5",
"prettier": "1.12.1",
"proxyquire": "^2.0.1",
"sinon": "^5.0.1",
"testdouble": "^3.8.0",
"ts-node": "^6.0.2",
"tslint": "^5.9.1",
"sinon": "^5.0.4",
"testdouble": "^3.8.1",
"ts-node": "^6.0.3",
"tslint": "^5.10.0",
"typescript": "2.8.3"
},
"engines": {
Expand Down
39 changes: 34 additions & 5 deletions src/api_client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as Config from '@oclif/config'
import {CLIError} from '@oclif/errors'
import {CLIError, warn} from '@oclif/errors'
import {HTTP, HTTPError, HTTPRequestOptions} from 'http-call'
import Netrc from 'netrc-parser'
import * as url from 'url'

import deps from './deps'
import {Login} from './login'
import {Mutex} from './mutex'
import {vars} from './vars'

Expand Down Expand Up @@ -40,7 +42,9 @@ export class HerokuAPIError extends CLIError {

export class APIClient {
preauthPromises: { [k: string]: Promise<HTTP> }
authPromise?: Promise<HTTP>
http: typeof HTTP
private readonly _login = new Login(this.config, this)
private _twoFactorMutex: Mutex<string> | undefined
private _auth?: string

Expand All @@ -52,7 +56,6 @@ export class APIClient {
let apiUrl = url.URL ? new url.URL(vars.apiUrl) : url.parse(vars.apiUrl)
let envHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}')
this.preauthPromises = {}
let auth = this.auth
let self = this as any
const opts = {
host: apiUrl.host,
Expand All @@ -62,7 +65,6 @@ export class APIClient {
...envHeaders,
},
}
if (auth && !opts.headers.authorization) opts.headers.authorization = `Bearer ${auth}`
this.http = class APIHTTPClient extends deps.HTTP.HTTP.create(opts) {
static async twoFactorRetry(
err: HTTPError,
Expand All @@ -87,13 +89,23 @@ export class APIClient {
}
}

static async request(url: string, opts: HTTPRequestOptions = {}, retries = 3) {
static async request(url: string, opts: HTTPRequestOptions = {}, retries = 3): Promise<APIHTTPClient> {
opts.headers = opts.headers || {}
if (!opts.headers.authorization) {
opts.headers.authorization = `Bearer ${self.auth}`
}
retries--
try {
return await super.request(url, opts)
} catch (err) {
if (!(err instanceof deps.HTTP.HTTPError)) throw err
if (retries > 0) {
if (err.http.statusCode === 401 && err.body.id === 'unauthorized') {
if (!self.authPromise) self.authPromise = self.login()
await self.authPromise
opts.headers.authorization = `Bearer ${self.auth}`
return this.request(url, opts, retries)
}
if (err.http.statusCode === 403 && err.body.id === 'two_factor') {
return this.twoFactorRetry(err, url, opts, retries)
}
Expand Down Expand Up @@ -122,6 +134,10 @@ export class APIClient {
}
return this._auth
}
set auth(token: string | undefined) {
delete this.authPromise
this._auth = token
}

twoFactorPrompt() {
deps.yubikey.enable()
Expand Down Expand Up @@ -161,7 +177,20 @@ export class APIClient {
return this.http.stream(url, options)
}
request(url: string, options: HTTPRequestOptions = {}) {
return this.http.request(url, options)
this.http.request(url, options)
}
login(opts: Login.Options = {}) {
return this._login.login(opts)
}
async logout() {
try {
await this._login.logout()
} catch (err) {
warn(err)
}
delete Netrc.machines['api.heroku.com']
delete Netrc.machines['git.heroku.com']
await Netrc.save()
}
get defaults(): typeof HTTP.defaults {
return this.http.defaults
Expand Down
1 change: 1 addition & 0 deletions src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function debug(...args: any[]) {
}

export function exists(f: string): Promise<boolean> {
// tslint:disable-next-line
return promisify(fs.exists)(f)
}

Expand Down
Loading

0 comments on commit c772322

Please sign in to comment.