Skip to content

Commit

Permalink
feat: added support for logging in to different environments
Browse files Browse the repository at this point in the history
  • Loading branch information
Fredx87 committed Jul 26, 2023
1 parent 3d39815 commit c90f0b3
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 46 deletions.
7 changes: 7 additions & 0 deletions docs/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ OPTIONS
-h, --help show CLI help
-i, --interactive Use Terminal based login
-s, --subdomain=subdomain Zendesk Subdomain
-d, --domain=domain Zendesk Domain (optional)
EXAMPLES
$ zcli login -i
$ zcli login -s zendesk-subdomain -i
$ zcli login -s zendesk-subdomain -d example.com -i
$ zcli login -s zendesk-subdomain -d dev.example.com -i
$ zcli login -d example.com -i
```

NOTE: For development purposes, you can specify a domain different from `zendesk.com` for logging in to a different environment. For example, if the environment is hosted on `example.com`, you can run
`zcli login -s zendesk-subdomain -d example.com -i` and you will be logged in to `zendesk-subdomain.example.com`. If the option is not specified, the default `zendesk.com` domain will be used.

NOTE: For CI/CD or unattended login you can set `ZENDESK_SUBDOMAIN`, `ZENDESK_EMAIL` and `ZENDESK_API_TOKEN` environment variables. You don't need to run login command if you have set these environment variables.
You can also set the `ZENDESK_DOMAIN` environment variable for different environments.
22 changes: 13 additions & 9 deletions docs/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
manage cli user profiles

* [`zcli profiles:list`](#zcli-profileslist)
* [`zcli profiles:remove SUBDOMAIN`](#zcli-profilesremove-subdomain)
* [`zcli profiles:use SUBDOMAIN`](#zcli-profilesuse-subdomain)
* [`zcli profiles:remove ACCOUNT`](#zcli-profilesremove-account)
* [`zcli profiles:use ACCOUNT`](#zcli-profilesuse-account)

Note: `ACCOUNT` means `subdomain` if you logged in using only the subdomain or `subdomain.domain` if you logged in to an environment hosted on a different domain

## `zcli profiles:list`

Expand All @@ -16,29 +18,31 @@ USAGE
$ zcli profiles:list
EXAMPLE
$ zcli profiles
$ zcli profiles:list
```

## `zcli profiles:remove SUBDOMAIN`
## `zcli profiles:remove ACCOUNT`

removes a profile

```
USAGE
$ zcli profiles:remove SUBDOMAIN
$ zcli profiles:remove ACCOUNT
EXAMPLE
$ zcli profiles:remove [SUBDOMAIN]
$ zcli profiles:remove zendesk-subdomain
$ zcli profiles:remove zendesk-subdomain.example.com
```

## `zcli profiles:use SUBDOMAIN`
## `zcli profiles:use ACCOUNT`

switches to a profile

```
USAGE
$ zcli profiles:use SUBDOMAIN
$ zcli profiles:use ACCOUNT
EXAMPLE
$ zcli profiles:use [SUBDOMAIN]
$ zcli profiles:use zendesk-subdomain
$ zcli profiles:use zendesk-subdomain.example.com
```
1 change: 1 addition & 0 deletions packages/zcli-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as Auth } from './lib/auth'
export { default as Config } from './lib/config'
export { default as SecureStore } from './lib/secureStore'
export { getBaseUrl } from './lib/requestUtils'
export * as request from './lib/request'
export * as env from './lib/env'
46 changes: 46 additions & 0 deletions packages/zcli-core/src/lib/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CliUx } from '@oclif/core'
import * as chalk from 'chalk'
import Auth from './auth'
import SecureStore from './secureStore'
import { Profile } from '../types'

const mockCreateBasicAuthToken = (...args: any[]) => {
return `Basic ${args[0]}_${args[1]}_base64`
Expand Down Expand Up @@ -105,6 +106,51 @@ describe('Auth', () => {
expect(await auth.loginInteractively()).to.equal(true)
})

test
.do(() => {
promptStub.reset()
promptStub.onFirstCall().resolves('z3ntest')
promptStub.onSecondCall().resolves('test@zendesk.com')
promptStub.onThirdCall().resolves('123456')
})
.stub(CliUx.ux, 'prompt', () => promptStub)
.stub(auth.secureStore, 'setPassword', () => Promise.resolve())
.stub(auth, 'setLoggedInProfile', () => Promise.resolve())
.stub(auth, 'createBasicAuthToken', mockCreateBasicAuthToken)
.nock('https://z3ntest.example.com', api => {
api
.get('/api/v2/account/settings.json')
.reply(function () {
expect(this.req.headers.authorization).to.equal('Basic test@zendesk.com_123456_base64')
return [200]
})
})
.it('should login successfully using the passed domain and the prompted subdomain', async () => {
expect(await auth.loginInteractively({ domain: 'example.com' } as Profile)).to.equal(true)
})

test
.do(() => {
promptStub.reset()
promptStub.onFirstCall().resolves('test@zendesk.com')
promptStub.onSecondCall().resolves('123456')
})
.stub(CliUx.ux, 'prompt', () => promptStub)
.stub(auth.secureStore, 'setPassword', () => Promise.resolve())
.stub(auth, 'setLoggedInProfile', () => Promise.resolve())
.stub(auth, 'createBasicAuthToken', mockCreateBasicAuthToken)
.nock('https://z3ntest.example.com', api => {
api
.get('/api/v2/account/settings.json')
.reply(function () {
expect(this.req.headers.authorization).to.equal('Basic test@zendesk.com_123456_base64')
return [200]
})
})
.it('should login successfully using the passed subdomain and domain', async () => {
expect(await auth.loginInteractively({ subdomain: 'z3ntest', domain: 'example.com' })).to.equal(true)
})

test
.do(() => {
promptStub.reset()
Expand Down
20 changes: 12 additions & 8 deletions packages/zcli-core/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import Config from './config'
import axios from 'axios'
import SecureStore from './secureStore'
import { Profile } from '../types'
import { parseSubdomain } from './authUtils'
import { getAccount, parseSubdomain } from './authUtils'
import { getBaseUrl } from './requestUtils'

export interface AuthOptions {
secureStore: SecureStore;
Expand Down Expand Up @@ -33,7 +34,7 @@ export default class Auth {
} else {
const profile = await this.getLoggedInProfile()
if (profile && this.secureStore) {
const authToken = await this.secureStore.getPassword(profile.subdomain)
const authToken = await this.secureStore.getPassword(getAccount(profile.subdomain, profile.domain))
return authToken
}

Expand All @@ -50,26 +51,29 @@ export default class Auth {
return this.config.getConfig('activeProfile') as unknown as Profile
}

setLoggedInProfile (subdomain: string) {
return this.config.setConfig('activeProfile', { subdomain })
setLoggedInProfile (subdomain: string, domain?: string) {
return this.config.setConfig('activeProfile', { subdomain, domain })
}

async loginInteractively (options?: Profile) {
const subdomain = parseSubdomain(options?.subdomain || await CliUx.ux.prompt('Subdomain'))
const domain = options?.domain
const account = getAccount(subdomain, domain)
const baseUrl = getBaseUrl(subdomain, domain)
const email = await CliUx.ux.prompt('Email')
const password = await CliUx.ux.prompt('Password', { type: 'hide' })

const authToken = this.createBasicAuthToken(email, password)
const testAuth = await axios.get(
`https://${subdomain}.zendesk.com/api/v2/account/settings.json`,
`${baseUrl}/api/v2/account/settings.json`,
{
headers: { Authorization: authToken },
validateStatus: function (status) { return status < 500 }
})

if (testAuth.status === 200 && this.secureStore) {
await this.secureStore.setPassword(subdomain, authToken)
await this.setLoggedInProfile(subdomain)
await this.secureStore.setPassword(account, authToken)
await this.setLoggedInProfile(subdomain, domain)

return true
}
Expand All @@ -85,7 +89,7 @@ export default class Auth {
const profile = await this.getLoggedInProfile()
if (!profile?.subdomain) throw new CLIError(chalk.red('Failed to log out: no active profile found.'))
await this.config.removeConfig('activeProfile')
const deleted = await this.secureStore.deletePassword(profile.subdomain)
const deleted = await this.secureStore.deletePassword(getAccount(profile.subdomain, profile.domain))
if (!deleted) throw new CLIError(chalk.red('Failed to log out: Account, Service not found.'))

return true
Expand Down
17 changes: 16 additions & 1 deletion packages/zcli-core/src/lib/authUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@oclif/test'
import { parseSubdomain } from './authUtils'
import { getAccount, getProfileFromAccount, parseSubdomain } from './authUtils'

describe('authUtils', () => {
describe('parseSubdomain', () => {
Expand All @@ -11,4 +11,19 @@ describe('authUtils', () => {
expect(parseSubdomain('test4')).to.equal('test4')
})
})

describe('getAccount', () => {
test.it('should get the account from subdomain and eventually domain', () => {
expect(getAccount('test')).to.equal('test')
expect(getAccount('test', 'example.com')).to.equal('test.example.com')
})
})

describe('getProfileFromAccount', () => {
test.it('should get the profile from account', () => {
expect(getProfileFromAccount('test')).to.deep.equal({ subdomain: 'test' })
expect(getProfileFromAccount('test.example.com')).to.deep.equal({ subdomain: 'test', domain: 'example.com' })
expect(getProfileFromAccount('test.subdomain.example.com')).to.deep.equal({ subdomain: 'test', domain: 'subdomain.example.com' })
})
})
})
14 changes: 14 additions & 0 deletions packages/zcli-core/src/lib/authUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Profile } from '../types'

/**
* Parse a subdomain.
*
Expand All @@ -13,3 +15,15 @@ export const parseSubdomain = (subdomain: string) => {
const result = regex.exec(subdomain)
return result !== null ? result[1] : subdomain
}

export const getAccount = (subdomain: string, domain?: string): string => {
return domain ? `${subdomain}.${domain}` : subdomain
}

export const getProfileFromAccount = (account: string): Profile => {
const firstDotIndex = account.indexOf('.')
if (firstDotIndex === -1) {
return { subdomain: account }
}
return { subdomain: account.substring(0, firstDotIndex), domain: account.substring(firstDotIndex + 1) }
}
1 change: 1 addition & 0 deletions packages/zcli-core/src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const EnvVars = {
SUBDOMAIN: 'ZENDESK_SUBDOMAIN',
DOMAIN: 'ZENDESK_DOMAIN',
EMAIL: 'ZENDESK_EMAIL',
PASSWORD: 'ZENDESK_PASSWORD',
API_TOKEN: 'ZENDESK_API_TOKEN',
Expand Down
42 changes: 42 additions & 0 deletions packages/zcli-core/src/lib/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,46 @@ describe('requestAPI', () => {
})
})
.it('should be able to attach extra headers to request')

test.env({
ZENDESK_SUBDOMAIN: 'z3ntest',
ZENDESK_DOMAIN: 'example.com',
ZENDESK_EMAIL: 'test@zendesk.com',
ZENDESK_API_TOKEN: '123456'
})
.stub(Auth, 'getAuthorizationToken', () => Promise.resolve('token'))
.nock('https://z3ntest.example.com', api => {
api
.get('/api/v2/me')
.reply(function () {
expect(this.req.headers.authorization).to.equal('Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=')
return [200]
})
}).do(async () => {
await requestAPI('api/v2/me', {
method: 'GET'
})
})
.it('should make a request to the correct domain')

test.env({
ZENDESK_SUBDOMAIN: 'z3ntest',
ZENDESK_EMAIL: 'test@zendesk.com',
ZENDESK_API_TOKEN: '123456'
})
.stub(Auth, 'getAuthorizationToken', () => Promise.resolve('token'))
.stub(Auth, 'getLoggedInProfile', () => ({ subdomain: 'z3ntest2', domain: 'example.com' }))
.nock('https://z3ntest.zendesk.com', api => {
api
.get('/api/v2/me')
.reply(function () {
expect(this.req.headers.authorization).to.equal('Basic dGVzdEB6ZW5kZXNrLmNvbS90b2tlbjoxMjM0NTY=')
return [200]
})
}).do(async () => {
await requestAPI('api/v2/me', {
method: 'GET'
})
})
.it('should not use the domain stored in current in profile if ZENDESK_SUBDOMAIN is provided and ZENDESK_DOMAIN is not provided')
})
5 changes: 3 additions & 2 deletions packages/zcli-core/src/lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Auth from './auth'
import { CLIError } from '@oclif/core/lib/errors'
import * as chalk from 'chalk'
import { EnvVars, varExists } from './env'
import { getSubdomain } from './requestUtils'
import { getBaseUrl, getDomain, getSubdomain } from './requestUtils'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const requestAPI = async (url: string, options: any = {}, json = false) => {
Expand All @@ -23,6 +23,7 @@ export const requestAPI = async (url: string, options: any = {}, json = false) =

const authToken = await auth.getAuthorizationToken()
const subdomain = process.env[EnvVars.SUBDOMAIN] || (await getSubdomain(auth))
const domain = process.env[EnvVars.SUBDOMAIN] ? (process.env[EnvVars.DOMAIN] || undefined) : await getDomain(auth)

if (options.headers) {
options.headers = { Authorization: authToken, ...options.headers }
Expand All @@ -32,7 +33,7 @@ export const requestAPI = async (url: string, options: any = {}, json = false) =

if (authToken && subdomain) {
return axios.request({
baseURL: `https://${subdomain}.zendesk.com`,
baseURL: getBaseUrl(subdomain, domain),
url,
validateStatus: function (status) { return status < 500 },
...options
Expand Down
15 changes: 15 additions & 0 deletions packages/zcli-core/src/lib/requestUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from '@oclif/test'
import { getBaseUrl } from './requestUtils'

describe('requestUtils', () => {
describe('getBaseUrl', () => {
test
.it('should get baseUrl from subdomain and domain', async () => {
expect(getBaseUrl('test')).to.equal('https://test.zendesk.com')
expect(getBaseUrl('test', undefined)).to.equal('https://test.zendesk.com')
expect(getBaseUrl('test', '')).to.equal('https://test.zendesk.com')
expect(getBaseUrl('test', 'example.com')).to.equal('https://test.example.com')
expect(getBaseUrl('test', 'subdomain.example.com')).to.equal('https://test.subdomain.example.com')
})
})
})
8 changes: 8 additions & 0 deletions packages/zcli-core/src/lib/requestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ import Auth from './auth'
export const getSubdomain = async (auth: Auth): Promise<string> => {
return (await auth.getLoggedInProfile())?.subdomain
}

export const getDomain = async (auth: Auth): Promise<string | undefined> => {
return (await auth.getLoggedInProfile())?.domain
}

export const getBaseUrl = (subdomain: string, domain?: string): string => {
return `https://${subdomain}.${domain || 'zendesk.com'}`
}
2 changes: 1 addition & 1 deletion packages/zcli-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export interface KeyTar {
findCredentials: (service: string) => Promise<Array<Credential>>;
}

export interface Profile { subdomain: string }
export interface Profile { subdomain: string, domain?: string }

// End profiles and Cred Store definitions
9 changes: 5 additions & 4 deletions packages/zcli-themes/src/commands/themes/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as morgan from 'morgan'
import * as chalk from 'chalk'
import * as cors from 'cors'
import * as chokidar from 'chokidar'
import { Auth } from '@zendesk/zcli-core'
import { Auth, getBaseUrl } from '@zendesk/zcli-core'
import preview from '../../lib/preview'
import getManifest from '../../lib/getManifest'
import getVariables from '../../lib/getVariables'
Expand Down Expand Up @@ -77,9 +77,10 @@ export default class Preview extends Command {
server.listen(port, host, async () => {
// preview requires authentication so we're sure
// to have a logged in profile at this point
const { subdomain } = await new Auth().getLoggedInProfile()
this.log(chalk.bold.green('Ready', chalk.blueBright(`https://${subdomain}.zendesk.com/hc/admin/local_preview/start`, '🚀')))
this.log(`You can exit preview mode in the UI or by visiting https://${subdomain}.zendesk.com/hc/admin/local_preview/stop`)
const { subdomain, domain } = await new Auth().getLoggedInProfile()
const baseUrl = getBaseUrl(subdomain, domain)
this.log(chalk.bold.green('Ready', chalk.blueBright(`${baseUrl}/hc/admin/local_preview/start`, '🚀')))
this.log(`You can exit preview mode in the UI or by visiting ${baseUrl}/hc/admin/local_preview/stop`)
tailLogs && this.log(chalk.bold('Tailing logs'))
})

Expand Down
Loading

0 comments on commit c90f0b3

Please sign in to comment.