From 32e01f30581f8bfbeb33ddf8e8b1aee898aacdd7 Mon Sep 17 00:00:00 2001 From: Zachary Golba Date: Tue, 24 Sep 2019 16:38:39 -0400 Subject: [PATCH 1/4] feat: support sandbox instances of salesforce --- packages/glide-authenticate/serverless.yml | 8 +-- .../glide-authenticate/src/events/connect.ts | 5 ++ .../glide-authenticate/src/events/message.ts | 15 +++++ .../src/events/oauth/callback.ts | 26 ++++++++ .../src/events/oauth/refresh.ts | 13 ++++ packages/glide-authenticate/src/lib.ts | 60 ------------------- packages/glide-authenticate/src/types.ts | 22 +++++++ packages/glide-authenticate/src/utilities.ts | 30 +++++++++- src/commands/init.ts | 14 +++-- src/commands/serve.ts | 15 ++++- src/lib.ts | 6 ++ src/main.ts | 1 + src/oauth.ts | 23 +++++-- 13 files changed, 158 insertions(+), 80 deletions(-) create mode 100644 packages/glide-authenticate/src/events/connect.ts create mode 100644 packages/glide-authenticate/src/events/message.ts create mode 100644 packages/glide-authenticate/src/events/oauth/callback.ts create mode 100644 packages/glide-authenticate/src/events/oauth/refresh.ts create mode 100644 packages/glide-authenticate/src/types.ts diff --git a/packages/glide-authenticate/serverless.yml b/packages/glide-authenticate/serverless.yml index 8d633ea..d322ff0 100644 --- a/packages/glide-authenticate/serverless.yml +++ b/packages/glide-authenticate/serverless.yml @@ -7,23 +7,23 @@ package: - dist/** functions: callback: - handler: dist/lib.callback + handler: dist/events/oauth/callback.default events: - http: path: oauth/callback method: get connect: - handler: dist/lib.connect + handler: dist/events/connect.default events: - websocket: route: $connect message: - handler: dist/lib.message + handler: dist/events/message.default events: - websocket: route: $default refresh: - handler: dist/lib.refresh + handler: dist/events/oauth/refresh.default events: - http: path: oauth/refresh diff --git a/packages/glide-authenticate/src/events/connect.ts b/packages/glide-authenticate/src/events/connect.ts new file mode 100644 index 0000000..4b1b433 --- /dev/null +++ b/packages/glide-authenticate/src/events/connect.ts @@ -0,0 +1,5 @@ +import { handler } from "../utilities"; + +export default handler(async () => { + // noop +}); diff --git a/packages/glide-authenticate/src/events/message.ts b/packages/glide-authenticate/src/events/message.ts new file mode 100644 index 0000000..f566de9 --- /dev/null +++ b/packages/glide-authenticate/src/events/message.ts @@ -0,0 +1,15 @@ +import { MessageType } from "../types"; +import { handler, params, oauth2, send } from "../utilities"; + +export default handler(async request => { + const { connectionId, domainName, stage } = request.requestContext; + const strategy = oauth2.configure(params.get(request, "environment")); + const result = strategy.getAuthorizationUrl({ + state: JSON.stringify({ connectionId, domainName, stage }), + }); + + await send(request.requestContext, { + data: result, + type: MessageType.Initialize, + }); +}); diff --git a/packages/glide-authenticate/src/events/oauth/callback.ts b/packages/glide-authenticate/src/events/oauth/callback.ts new file mode 100644 index 0000000..db937b2 --- /dev/null +++ b/packages/glide-authenticate/src/events/oauth/callback.ts @@ -0,0 +1,26 @@ +import { Connection } from "jsforce"; + +import { MessageType } from "../../types"; +import { handler, params, oauth2, send } from "../../utilities"; + +export const callback = handler(async request => { + const environment = params.get(request, "environment"); + const connection = new Connection({ + oauth2: oauth2.configure(environment), + }); + + await connection.authorize(params.require(request, "code")); + await send(JSON.parse(params.require(request, "state")), { + data: { + accessToken: connection.accessToken, + oauth2: oauth2.options(environment), + // @ts-ignore + refreshToken: connection.refreshToken, + }, + type: MessageType.Authenticated, + }); + + return { + message: "You have been successfully logged in", + }; +}); diff --git a/packages/glide-authenticate/src/events/oauth/refresh.ts b/packages/glide-authenticate/src/events/oauth/refresh.ts new file mode 100644 index 0000000..a984ec4 --- /dev/null +++ b/packages/glide-authenticate/src/events/oauth/refresh.ts @@ -0,0 +1,13 @@ +import { handler, oauth2 } from "../../utilities"; + +export const refresh = handler(async request => { + const { environment, token } = JSON.parse(request.body || "{}"); + const strategy = oauth2.configure(environment); + const result = await strategy.refreshToken(token); + + return { + oauth2: oauth2.options(environment), + accessToken: result.access_token, + refreshToken: result.refresh_token, + }; +}); diff --git a/packages/glide-authenticate/src/lib.ts b/packages/glide-authenticate/src/lib.ts index cb1bbdf..e69de29 100644 --- a/packages/glide-authenticate/src/lib.ts +++ b/packages/glide-authenticate/src/lib.ts @@ -1,60 +0,0 @@ -import { Connection, OAuth2 } from "jsforce"; -import nconf from "nconf"; - -import { handler, params, send } from "./utilities"; - -const oauth2 = new OAuth2(nconf.env("__").get("oauth")); - -const enum MessageType { - Authenticated = "AUTHENTICATED", - Initialize = "INITIALIZE", -} - -export const callback = handler(async request => { - const client = JSON.parse(params.require(request, "state")); - const connection = new Connection({ oauth2 }); - - await connection.authorize(params.require(request, "code")); - - await send(client, { - data: { - accessToken: connection.accessToken, - oauth2: nconf.get("oauth"), - // @ts-ignore - refreshToken: connection.refreshToken, - }, - type: MessageType.Authenticated, - }); - - return { - message: "You have been successfully logged in", - }; -}); - -export const connect = handler(async () => { - // noop -}); - -export const message = handler(async request => { - const state = JSON.stringify({ - connectionId: request.requestContext.connectionId, - domainName: request.requestContext.domainName, - stage: request.requestContext.stage, - }); - - await send(request.requestContext, { - data: oauth2.getAuthorizationUrl({ state }), - type: MessageType.Initialize, - }); -}); - -export const refresh = handler(async request => { - const { token } = JSON.parse(request.body || "{}"); - const results = await oauth2.refreshToken(token); - - return { - oauth2: nconf.get("oauth"), - accessToken: results.access_token, - refreshToken: results.refresh_token, - }; -}); diff --git a/packages/glide-authenticate/src/types.ts b/packages/glide-authenticate/src/types.ts new file mode 100644 index 0000000..d48d87c --- /dev/null +++ b/packages/glide-authenticate/src/types.ts @@ -0,0 +1,22 @@ +export enum Environment { + Default = "default", + Sandbox = "sandbox", +} + +export namespace Environment { + export function from(input?: string): Environment { + switch (input) { + case Environment.Sandbox: { + return Environment.Sandbox; + } + default: { + return Environment.Default; + } + } + } +} + +export const enum MessageType { + Authenticated = "AUTHENTICATED", + Initialize = "INITIALIZE", +} diff --git a/packages/glide-authenticate/src/utilities.ts b/packages/glide-authenticate/src/utilities.ts index db67f3f..ca2c4ee 100644 --- a/packages/glide-authenticate/src/utilities.ts +++ b/packages/glide-authenticate/src/utilities.ts @@ -1,5 +1,9 @@ import lambda, { Context } from "aws-lambda"; import { ApiGatewayManagementApi } from "aws-sdk"; +import { OAuth2, OAuth2Options } from "jsforce"; +import nconf from "nconf"; + +import { Environment } from "./types"; let apiGateway: ApiGatewayManagementApi | null = null; @@ -11,6 +15,8 @@ export type Response = lambda.APIGatewayProxyResult; export type Responder = (request: Request, context: Context) => Promise; export function handler(responder: Responder): lambda.APIGatewayProxyHandler { + nconf.env("__"); + return (request, context, callback) => { responder(request, context).then( data => { @@ -52,9 +58,29 @@ export async function send(client: Client, data: object): Promise { await apiGateway.postToConnection(message).promise(); } +export namespace oauth2 { + const url = { + [Environment.Default]: "https://login.salesforce.com", + [Environment.Sandbox]: "https://test.salesforce.com", + }; + + export function configure(environment?: string): OAuth2 { + return new OAuth2(options(environment)); + } + + export function options(environment?: string): OAuth2Options { + return { + ...nconf.get("oauth"), + loginUrl: url[Environment.from(environment)], + }; + } +} + export namespace params { - export function get(request: Request, param: string): string | undefined { - return (request.queryStringParameters || {})[param]; + export function get(request: Request, param: string): string | undefined; + export function get(request: Request, param: string, value: string): string; + export function get(request: Request, param: string, value?: string): string | undefined { + return (request.queryStringParameters || {})[param] || value; } export function require(request: Request, param: string): string { diff --git a/src/commands/init.ts b/src/commands/init.ts index 8a774f7..400f283 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,10 +1,11 @@ // tslint:disable: object-shorthand-properties-first import { Definition, Operator } from "@glide/runtime"; +import { Command } from "commander"; import jsforce from "jsforce"; import { Options } from "../lib"; -import { login } from "../oauth"; +import { Environment, login } from "../oauth"; import { json, string } from "../utilities"; interface Identifiers { @@ -18,9 +19,10 @@ interface Inflection { singular: string; } -export default async function init(origin: string, path: string = "glide.json"): Promise { - const { instance, schema } = createOptions(origin); - const connection = await login(instance); +export default async function init(origin: string, path: string, flags: Command): Promise { + const { instance, sandbox, schema } = createOptions(origin, flags); + const environment = sandbox ? Environment.Default : Environment.Sandbox; + const connection = await login(instance, environment); const sobjects = await connection.describeGlobal().then(({ sobjects }) => { return Promise.all(sobjects.map(({ name }) => connection.describe(name))); }); @@ -55,13 +57,15 @@ export default async function init(origin: string, path: string = "glide.json"): await json.write(path, { instance, + sandbox, schema, }); } -function createOptions(instance: string): Options { +function createOptions(instance: string, flags: Command): Options { return { instance, + sandbox: Boolean(flags.sandbox), schema: { mutations: {}, queries: {}, diff --git a/src/commands/serve.ts b/src/commands/serve.ts index 69a2226..fc9c6f8 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -1,7 +1,8 @@ +import { Connection } from "jsforce"; import open from "open"; import glide, { Options } from "../lib"; -import { login } from "../oauth"; +import { Environment, login } from "../oauth"; import { display, json, isDevEnv } from "../utilities"; export interface Flags { @@ -16,7 +17,7 @@ export default async function serve(path: string = "glide.json", flags: Flags): const listener = glide(options).listen(flags.port, () => { const address = display.address(listener); - console.log(`server listening on ${address}`); + console.log(`serving objects from ${options.instance} on ${address}`); if (isDevEnv()) { open(address).then(browser => browser.unref()); @@ -35,9 +36,17 @@ export default async function serve(path: string = "glide.json", flags: Flags): async function configure(path: string): Promise { const options = await json.read(path); + let connection: Connection | null = null; + + if (isDevEnv()) { + connection = await login( + options.instance, + options.sandbox ? Environment.Default : Environment.Sandbox, + ); + } return { - connection: isDevEnv() ? await login(options.instance) : null, + connection, ...options, }; } diff --git a/src/lib.ts b/src/lib.ts index f3b5bb2..e9f8b00 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -34,6 +34,12 @@ export interface Options { */ instance: string; + /** + * If `true`, `https://test.salesforce.com` will be used in place of + * `https://login.salesforce.com` as a login url. + */ + sandbox?: boolean; + /** * A schema defintion that describes the shape of your Salesforce instance. */ diff --git a/src/main.ts b/src/main.ts index 5de94fa..4dc85f5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,7 @@ export default function main(): void { commander .command("init [path]") .description("generates a glide.json file in the current directory") + .option("-s, --sandbox", "should be set if the given url points to a salesforce sandbox") .action(subcommand(init)); commander diff --git a/src/oauth.ts b/src/oauth.ts index c40c0b1..f665c3a 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -12,6 +12,11 @@ type Message = | Readonly<{ data: Session; type: MessageType.Authenticated }> | Readonly<{ data: string; type: MessageType.Initialize }>; +interface RefreshParams { + readonly environment?: Environment; + readonly token: string; +} + interface Refresh { readonly data?: Session; readonly errors?: [Pick]; @@ -32,11 +37,19 @@ const enum MessageType { Initialize = "INITIALIZE", } -export async function login(instance: string): Promise { +export const enum Environment { + Default = "default", + Sandbox = "sandbox", +} + +export async function login(instance: string, environment: Environment): Promise { const credentials = await loadCredentials(); if (credentials[instance]) { - return refreshToken(instance, credentials[instance]); + return refreshToken(instance, { + environment, + token: credentials[instance], + }); } return new Promise((resolve, reject) => { @@ -102,10 +115,8 @@ async function loadCredentials(): Promise { return credentials; } -async function refreshToken(instance: string, token: string): Promise { - const [{ data, errors }, response] = await json.post(Endpoint.Refresh, { - token, - }); +async function refreshToken(instance: string, params: RefreshParams): Promise { + const [{ data, errors }, response] = await json.post(Endpoint.Refresh, params); return response.ok ? createConnection(instance, data!) From b3d97bf5b25d1d7bf50e8908365daf371e6621f8 Mon Sep 17 00:00:00 2001 From: Zachary Golba Date: Wed, 25 Sep 2019 16:54:36 -0400 Subject: [PATCH 2/4] feat: use glide.sandbox.json as default when --sandbox is used --- package.json | 1 + src/commands/serve.ts | 28 +++++++++++++++++++--------- src/main.ts | 12 +++++++++++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ebdc92a..72482d8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@glide/runtime": "^0.0.1", "@zakgolba/jsonptr": "^1.1.0", "body-parser": "^1.19.0", + "boxen": "^4.1.0", "chalk": "^2.4.2", "commander": "^3.0.1", "cross-fetch": "^3.0.4", diff --git a/src/commands/serve.ts b/src/commands/serve.ts index fc9c6f8..0b9823b 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -1,4 +1,7 @@ -import { Connection } from "jsforce"; +import { EOL } from "os"; + +import boxen from "boxen"; +import chalk from "chalk"; import open from "open"; import glide, { Options } from "../lib"; @@ -16,8 +19,19 @@ export default async function serve(path: string = "glide.json", flags: Flags): const options = await configure(path); const listener = glide(options).listen(flags.port, () => { const address = display.address(listener); - - console.log(`serving objects from ${options.instance} on ${address}`); + const message = [ + chalk`Config File: {underline ${path}}`, + chalk`GraphQL Server: {underline ${address}}`, + chalk`Salesforce Instance: {underline ${options.instance}}`, + ]; + + console.log( + boxen(message.join(EOL), { + float: "center", + margin: 3, + padding: 1, + }), + ); if (isDevEnv()) { open(address).then(browser => browser.unref()); @@ -36,17 +50,13 @@ export default async function serve(path: string = "glide.json", flags: Flags): async function configure(path: string): Promise { const options = await json.read(path); - let connection: Connection | null = null; if (isDevEnv()) { - connection = await login( + options.connection = await login( options.instance, options.sandbox ? Environment.Default : Environment.Sandbox, ); } - return { - connection, - ...options, - }; + return options; } diff --git a/src/main.ts b/src/main.ts index 4dc85f5..50c943d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,7 +15,17 @@ export default function main(): void { .command("init [path]") .description("generates a glide.json file in the current directory") .option("-s, --sandbox", "should be set if the given url points to a salesforce sandbox") - .action(subcommand(init)); + .action( + subcommand(async (origin, path, command) => { + let output = path; + + if (output == null) { + output = command.sandbox ? "glide.sandbox.json" : "glide.json"; + } + + await init(origin, output, command); + }), + ); commander .command("print [path]") From dfb604e68abebc2293a363bde941122447991494 Mon Sep 17 00:00:00 2001 From: Zachary Golba Date: Wed, 25 Sep 2019 16:55:04 -0400 Subject: [PATCH 3/4] chore(docs): update getting started guide --- documentation/getting-started.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/documentation/getting-started.md b/documentation/getting-started.md index 77f8ee3..75e0fe7 100644 --- a/documentation/getting-started.md +++ b/documentation/getting-started.md @@ -40,6 +40,20 @@ When starting a new project or adding glide to an existing project, you may exec glide init https://my-salesforce-instance.salesforce.com ``` +### Sandbox Setup + +If you wish to use glide with a sandbox environment, you may generate an additional JSON configuration file by running the `init` command again with the `--sandbox` option. When `--sandbox` is passed to the `init` command, a boolean `"sandbox": true` property is added to the resulting JSON configuration file. This option instructs the authentication layer of glide to use the correct login url (https://test.salesforce.com). + +```sh +glide init https://my-salesforce-sandbox.salesforce.com --sandbox +``` + +In order to prevent name collisions with the default JSON configuration file generated from the `init` command, `glide.sandbox.json` will be used as the file name if the optional `path` argument is not provided. If you wish to use another file name or path, you may do so by providing the `path` argument. + +```sh +glide init https://my-salesforce-sandbox.salesforce.com sandbox.json --sandbox +``` + **Note:** If you have not used Glide with your Salesforce instance before, you may be prompted to login. More information about how authentication works in Glide can be found in the [_Advanced Topics_](./advanced-topics.md) section of the guides. @@ -48,6 +62,16 @@ If you have not used Glide with your Salesforce instance before, you may be prom Once you have a JSON configuration file in the root directory of your project, you may spawn a GraphQL server by executing the `serve` command. When executing the `serve` command in a development environment, your default browser will open a [GraphQL Playground](https://github.com/prisma/graphql-playground) playground page to start exploring your data. +```sh +glide serve +``` + +To use a different JSON configuration file then the default `glide.json` generated by the `init` command, you may do so by providing a the path to the configuration file as a positional argument to the `serve` command. + +```sh +glide serve glide.sandbox.json +``` + You manually edit your JSON configuration file if you wish to modify the mutations, queries, and/or data types exposed by your GraphQL server. Information about how to configure the generated GraphQL schema can be found in the [_Advanced Topics_](./advanced-topics.md) section of the guides. GraphQL Playground From f70af51d2889e2e492721591085378c2da344ebd Mon Sep 17 00:00:00 2001 From: Zachary Golba Date: Thu, 26 Sep 2019 15:45:47 -0400 Subject: [PATCH 4/4] fix: environment based auth logic errors --- packages/glide-authenticate/package.json | 3 +++ packages/glide-authenticate/serverless.yml | 2 +- packages/glide-authenticate/src/events/message.ts | 8 +++++--- packages/glide-authenticate/src/events/oauth/callback.ts | 2 +- packages/glide-authenticate/src/events/oauth/refresh.ts | 5 ++--- src/commands/init.ts | 2 +- src/commands/serve.ts | 2 +- src/oauth.ts | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/glide-authenticate/package.json b/packages/glide-authenticate/package.json index 1f8e450..e8a902b 100644 --- a/packages/glide-authenticate/package.json +++ b/packages/glide-authenticate/package.json @@ -5,17 +5,20 @@ "main": "dist/lib.js", "types": "dist/lib.d.ts", "scripts": { + "deploy": "serverless deploy", "lint": "tslint --project tsconfig.json" }, "dependencies": { "aws-sdk": "^2.534.0", "jsforce": "^1.9.2", + "lodash": "^4.17.15", "nconf": "^0.10.0" }, "devDependencies": { "@types/aws-lambda": "8.10.32", "@types/aws-sdk": "2.7.0", "@types/jsforce": "1.9.9", + "@types/lodash": "4.14.139", "@types/nconf": "0.10.0", "@types/node": "12.7.5", "serverless": "^1.52.2" diff --git a/packages/glide-authenticate/serverless.yml b/packages/glide-authenticate/serverless.yml index d322ff0..234bde4 100644 --- a/packages/glide-authenticate/serverless.yml +++ b/packages/glide-authenticate/serverless.yml @@ -1,4 +1,4 @@ -service: glide-authenticate +service: glide-oauth provider: name: aws runtime: nodejs8.10 diff --git a/packages/glide-authenticate/src/events/message.ts b/packages/glide-authenticate/src/events/message.ts index f566de9..49b0f4c 100644 --- a/packages/glide-authenticate/src/events/message.ts +++ b/packages/glide-authenticate/src/events/message.ts @@ -1,10 +1,12 @@ +import { get } from "lodash"; + import { MessageType } from "../types"; -import { handler, params, oauth2, send } from "../utilities"; +import { handler, oauth2, send } from "../utilities"; export default handler(async request => { const { connectionId, domainName, stage } = request.requestContext; - const strategy = oauth2.configure(params.get(request, "environment")); - const result = strategy.getAuthorizationUrl({ + const environment = get(JSON.parse(request.body || "{}"), "data.environment"); + const result = oauth2.configure(environment).getAuthorizationUrl({ state: JSON.stringify({ connectionId, domainName, stage }), }); diff --git a/packages/glide-authenticate/src/events/oauth/callback.ts b/packages/glide-authenticate/src/events/oauth/callback.ts index db937b2..c16f441 100644 --- a/packages/glide-authenticate/src/events/oauth/callback.ts +++ b/packages/glide-authenticate/src/events/oauth/callback.ts @@ -3,7 +3,7 @@ import { Connection } from "jsforce"; import { MessageType } from "../../types"; import { handler, params, oauth2, send } from "../../utilities"; -export const callback = handler(async request => { +export default handler(async request => { const environment = params.get(request, "environment"); const connection = new Connection({ oauth2: oauth2.configure(environment), diff --git a/packages/glide-authenticate/src/events/oauth/refresh.ts b/packages/glide-authenticate/src/events/oauth/refresh.ts index a984ec4..910e835 100644 --- a/packages/glide-authenticate/src/events/oauth/refresh.ts +++ b/packages/glide-authenticate/src/events/oauth/refresh.ts @@ -1,9 +1,8 @@ import { handler, oauth2 } from "../../utilities"; -export const refresh = handler(async request => { +export default handler(async request => { const { environment, token } = JSON.parse(request.body || "{}"); - const strategy = oauth2.configure(environment); - const result = await strategy.refreshToken(token); + const result = await oauth2.configure(environment).refreshToken(token); return { oauth2: oauth2.options(environment), diff --git a/src/commands/init.ts b/src/commands/init.ts index 400f283..3b4c23a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -21,7 +21,7 @@ interface Inflection { export default async function init(origin: string, path: string, flags: Command): Promise { const { instance, sandbox, schema } = createOptions(origin, flags); - const environment = sandbox ? Environment.Default : Environment.Sandbox; + const environment = sandbox ? Environment.Sandbox : Environment.Default; const connection = await login(instance, environment); const sobjects = await connection.describeGlobal().then(({ sobjects }) => { return Promise.all(sobjects.map(({ name }) => connection.describe(name))); diff --git a/src/commands/serve.ts b/src/commands/serve.ts index 0b9823b..f044b62 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -54,7 +54,7 @@ async function configure(path: string): Promise { if (isDevEnv()) { options.connection = await login( options.instance, - options.sandbox ? Environment.Default : Environment.Sandbox, + options.sandbox ? Environment.Sandbox : Environment.Default, ); } diff --git a/src/oauth.ts b/src/oauth.ts index f665c3a..a0f6de7 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -58,7 +58,7 @@ export async function login(instance: string, environment: Environment): Promise socket.on("open", () => { socket.send( JSON.stringify({ - data: null, + data: { environment }, type: MessageType.Initialize, }), );