Fold EdgehillAPI, LegacyEdgehillAPI, NylasAPI together

This commit is contained in:
Ben Gotow 2017-07-31 20:20:01 -07:00
parent 20490a33b0
commit 5a8dab4c71
26 changed files with 388 additions and 483 deletions

View file

@ -2,7 +2,7 @@ import {shell} from 'electron'
import React from 'react';
import ReactDOM from 'react-dom';
import {RetinaImg} from 'nylas-component-kit';
import {NylasAPI, Actions} from 'nylas-exports';
import {NylasAPIRequest, Actions} from 'nylas-exports';
import OnboardingActions from '../onboarding-actions';
import {runAuthValidation} from '../onboarding-helpers';
@ -135,7 +135,7 @@ const CreatePageForForm = (FormComponent) => {
errorFieldNames.push('email');
errorFieldNames.push('username');
}
if (NylasAPI.TimeoutErrorCodes.includes(err.statusCode)) { // timeout
if (NylasAPIRequest.TimeoutErrorCodes.includes(err.statusCode)) { // timeout
errorMessage = "We were unable to reach your mail provider. Please try again."
}

View file

@ -5,7 +5,7 @@ const OnboardingActions = Reflux.createActions([
"setAccountType",
"moveToPreviousPage",
"moveToPage",
"authenticationJSONReceived",
"identityJSONReceived",
"accountJSONReceived",
]);

View file

@ -3,14 +3,12 @@
import crypto from 'crypto';
import {CommonProviderSettings} from 'imap-provider-settings';
import {
N1CloudAPI,
NylasAPI,
NylasAPIRequest,
RegExpUtils,
Utils,
MailsyncProcess,
} from 'nylas-exports';
const {makeRequest, rootURLForServer} = NylasAPIRequest;
const IMAP_FIELDS = new Set([
"imap_host",
@ -41,40 +39,32 @@ function base64url(inBuffer) {
.replace(/\//g, '_'); // Convert '/' to '_'
}
const NO_AUTH = { user: '', pass: '', sendImmediately: true };
export async function makeGmailOAuthRequest(sessionKey) {
const remoteRequest = new NylasAPIRequest({
api: N1CloudAPI,
options: {
path: `/auth/gmail/token?key=${sessionKey}`,
method: 'GET',
auth: NO_AUTH,
},
export function makeGmailOAuthRequest(sessionKey) {
return makeRequest({
server: 'accounts',
path: `/auth/gmail/token?key=${sessionKey}`,
method: 'GET',
auth: false,
});
return remoteRequest.run()
}
export async function authIMAPForGmail(tokenData) {
const localRequest = new NylasAPIRequest({
api: NylasAPI,
options: {
path: `/auth`,
method: 'POST',
auth: NO_AUTH,
timeout: 1000 * 90, // Connecting to IMAP could take up to 90 seconds, so we don't want to hang up too soon
body: {
email: tokenData.email_address,
name: tokenData.name,
provider: 'gmail',
settings: {
xoauth2: tokenData.resolved_settings.xoauth2,
expiry_date: tokenData.resolved_settings.expiry_date,
},
const localJSON = await makeRequest({
server: 'accounts',
path: `/auth`,
method: 'POST',
auth: false,
timeout: 1000 * 90, // Connecting to IMAP could take up to 90 seconds, so we don't want to hang up too soon
body: {
email: tokenData.email_address,
name: tokenData.name,
provider: 'gmail',
settings: {
xoauth2: tokenData.resolved_settings.xoauth2,
expiry_date: tokenData.resolved_settings.expiry_date,
},
},
})
const localJSON = await localRequest.run()
const account = Object.assign({}, localJSON);
account.localToken = localJSON.account_token;
account.cloudToken = tokenData.account_token;
@ -86,14 +76,14 @@ export function buildGmailSessionKey() {
}
export function buildGmailAuthURL(sessionKey) {
return `${N1CloudAPI.APIRoot}/auth/gmail?state=${sessionKey}`;
return `${rootURLForServer('accounts')}/auth/gmail?state=${sessionKey}`;
}
export function runAuthValidation(accountInfo) {
export async function runAuthValidation(accountInfo) {
const {username, type, email, name} = accountInfo;
const data = {
id: Utils.generateTempId(), // TODO BG: Server will decide account ids
id: 'temp',
provider: type,
name: name,
emailAddress: email,
@ -123,32 +113,23 @@ export function runAuthValidation(accountInfo) {
// If this succeeds, send the received code to N1 server to register the account
// Otherwise process the error message from the server and highlight UI as needed
const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), data);
return proc.test().then((accountJSON) => {
return accountJSON;
});
const {account} = await proc.test();
// TODO BG Re-enable cloud services
// return n1CloudIMAPAuthRequest.run().then((remoteJSON) => {
// const localSyncIMAPAuthRequest = new NylasAPIRequest({
// api: NylasAPI,
// options: {
// path: `/auth`,
// method: 'POST',
// timeout: 1000 * 180, // Same timeout as server timeout (most requests are faster than 90s, but server validation can be slow in some cases)
// body: data,
// auth: {
// user: '',
// pass: '',
// sendImmediately: true,
// },
// },
// })
// return localSyncIMAPAuthRequest.run().then((localJSON) => {
// const accountWithTokens = Object.assign({}, localJSON);
// accountWithTokens.localToken = localJSON.account_token;
// accountWithTokens.cloudToken = '';//remoteJSON.account_token;
// return accountWithTokens
// })
delete data.id;
const {id, account_token} = await makeRequest({
server: 'accounts',
path: `/auth`,
method: 'POST',
timeout: 1000 * 180, // Same timeout as server timeout (most requests are faster than 90s, but server validation can be slow in some cases)
body: data,
auth: false,
})
return {
account: Object.assign({}, account, {id}),
cloudToken: account_token,
};
}
export function isValidHost(value) {

View file

@ -18,13 +18,10 @@ class OnboardingStore extends NylasStore {
constructor() {
super();
NylasEnv.config.onDidChange('env', this._onEnvChanged);
this._onEnvChanged();
this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)
this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage)
this.listenTo(OnboardingActions.accountJSONReceived, this._onAccountJSONReceived)
this.listenTo(OnboardingActions.authenticationJSONReceived, this._onAuthenticationJSONReceived)
this.listenTo(OnboardingActions.identityJSONReceived, this._onIdentityJSONReceived)
this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);
this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);
ipcRenderer.on('set-account-type', (e, type) => {
@ -80,19 +77,6 @@ class OnboardingStore extends NylasStore {
}
}
_onEnvChanged = () => {
const env = NylasEnv.config.get('env')
if (['development', 'local'].includes(env)) {
this.welcomeRoot = "http://0.0.0.0:5555";
} else if (env === 'experimental') {
this.welcomeRoot = "https://www-experimental.nylas.com";
} else if (env === 'staging') {
this.welcomeRoot = "https://www-staging.nylas.com";
} else {
this.welcomeRoot = "https://nylas.com";
}
}
_onOnboardingComplete = () => {
// When account JSON is received, we want to notify external services
// that it succeeded. Unfortunately in this case we're likely to
@ -137,7 +121,7 @@ class OnboardingStore extends NylasStore {
this.trigger();
}
_onAuthenticationJSONReceived = async (json) => {
_onIdentityJSONReceived = async (json) => {
const isFirstAccount = AccountStore.accounts().length === 0;
await IdentityStore.saveIdentity(json);
@ -145,8 +129,8 @@ class OnboardingStore extends NylasStore {
setTimeout(() => {
if (isFirstAccount) {
this._onSetAccountInfo(Object.assign({}, this._accountInfo, {
name: `${json.firstname || ""} ${json.lastname || ""}`,
email: json.email,
name: `${json.firstName || ""} ${json.lastName || ""}`,
email: json.emailAddress,
}));
OnboardingActions.moveToPage('account-choose');
} else {

View file

@ -1,5 +1,5 @@
import React from 'react';
import {IdentityStore} from 'nylas-exports';
import {NylasAPIRequest} from 'nylas-exports';
import {Webview} from 'nylas-component-kit';
import OnboardingActions from './onboarding-actions';
@ -12,18 +12,18 @@ export default class AuthenticatePage extends React.Component {
_src() {
const n1Version = NylasEnv.getVersion();
return `${IdentityStore.URLRoot}/onboarding?utm_medium=N1&utm_source=OnboardingPage&N1_version=${n1Version}&client_edition=basic`
return `${NylasAPIRequest.rootURLForServer('identity')}/onboarding?utm_medium=N1&utm_source=OnboardingPage&N1_version=${n1Version}&client_edition=basic`
}
_onDidFinishLoad = (webview) => {
const receiveUserInfo = `
var a = document.querySelector('#pro-account');
var a = document.querySelector('#identity-result');
result = a ? a.innerText : null;
`;
webview.executeJavaScript(receiveUserInfo, false, (result) => {
this.setState({ready: true, webviewLoading: false});
if (result !== null) {
OnboardingActions.authenticationJSONReceived(JSON.parse(result));
OnboardingActions.identityJSONReceived(JSON.parse(atob(result)));
}
});

View file

@ -3,7 +3,7 @@ path = require 'path'
fs = require 'fs'
_ = require 'underscore'
{RetinaImg, Flexbox, ConfigPropContainer, NewsletterSignup} = require 'nylas-component-kit'
{EdgehillAPI, AccountStore} = require 'nylas-exports'
{AccountStore} = require 'nylas-exports'
OnboardingActions = require('./onboarding-actions').default
# NOTE: Temporarily copied from preferences module

View file

@ -1,5 +1,5 @@
# This file is in coffeescript just to use the existential operator!
{AccountStore, LegacyEdgehillAPI} = require 'nylas-exports'
{AccountStore} = require 'nylas-exports'
MAX_RETRY = 10
@ -12,7 +12,7 @@ module.exports = class ClearbitDataSource
return Promise.resolve(null)
new Promise (resolve, reject) =>
return; # TODO BG
req = LegacyEdgehillAPI.makeRequest({
req = LegacyEdXgehillAPI.makeRequest({
authWithNylasAPI: true
path: "/proxy/clearbit/#{@clearbitAPI()}/find?email=#{email}",
})

View file

@ -67,7 +67,7 @@ class PreferencesIdentity extends React.Component {
render() {
const {identity} = this.state;
const {firstname, lastname, email} = identity;
const {firstName, lastName, emailAddress} = identity;
const logout = () => Actions.logoutNylasIdentity()
@ -83,8 +83,8 @@ class PreferencesIdentity extends React.Component {
/>
</div>
<div className="identity-info">
<div className="name">{firstname} {lastname}</div>
<div className="email">{email}</div>
<div className="name">{firstName} {lastName}</div>
<div className="email">{emailAddress}</div>
<div className="identity-actions">
<OpenIdentityPageButton label="Account Details" path="/dashboard" source="Preferences" campaign="Dashboard" />
<div className="btn minor-width" onClick={logout}>Sign Out</div>

View file

@ -35,9 +35,10 @@
"enzyme": "2.9.1",
"event-kit": "^1.0.2",
"fs-plus": "^2.3.2",
"getmac": "^1.2.1",
"he": "1.1.0",
"is-online": "7.0.0",
"imap-provider-settings": "github:nylas/imap-provider-settings#2fdcd34d59b",
"is-online": "7.0.0",
"jasmine-json": "~0.0",
"jasmine-react-helpers": "^0.2",
"jasmine-reporters": "1.x.x",

View file

@ -1,6 +1,7 @@
import {ipcRenderer} from 'electron';
import {Utils, KeyManager, DatabaseWriter, SendFeatureUsageEventTask} from 'nylas-exports'
import IdentityStore from '../../src/flux/stores/identity-store'
import * as NylasAPIRequest from '../../src/flux/nylas-api-request'
const TEST_NYLAS_ID = "icihsnqh4pwujyqihlrj70vh"
const TEST_TOKEN = "test-token"
@ -67,13 +68,13 @@ describe("IdentityStore", function identityStoreSpec() {
it("can log a feature usage event", async () => {
spyOn(IdentityStore, "saveIdentity").andReturn(Promise.resolve());
spyOn(IdentityStore, "nylasIDRequest");
spyOn(NylasAPIRequest, "makeRequest");
IdentityStore._identity = this.identityJSON
IdentityStore._identity.token = TEST_TOKEN;
IdentityStore._onEnvChanged()
const t = new SendFeatureUsageEventTask("snooze");
await t.performRemote()
const opts = IdentityStore.nylasIDRequest.calls[0].args[0]
const opts = NylasAPIRequest.makeRequest.calls[0].args[0]
expect(opts).toEqual({
method: "POST",
url: "https://billing.nylas.com/api/feature_usage_event",
@ -104,7 +105,7 @@ describe("IdentityStore", function identityStoreSpec() {
});
});
describe("_fetchIdentity", () => {
describe("fetchIdentity", () => {
beforeEach(() => {
IdentityStore._identity = this.identityJSON;
spyOn(IdentityStore, "saveIdentity")
@ -115,12 +116,12 @@ describe("IdentityStore", function identityStoreSpec() {
it("saves the identity returned", async () => {
const resp = Utils.deepClone(this.identityJSON);
resp.feature_usage.feat.quota = 5
spyOn(IdentityStore, "nylasIDRequest").andCallFake(() => {
spyOn(NylasAPIRequest, "makeRequest").andCallFake(() => {
return Promise.resolve(resp)
})
await IdentityStore._fetchIdentity();
expect(IdentityStore.nylasIDRequest).toHaveBeenCalled();
const options = IdentityStore.nylasIDRequest.calls[0].args[0]
await IdentityStore.fetchIdentity();
expect(NylasAPIRequest.makeRequest).toHaveBeenCalled();
const options = NylasAPIRequest.makeRequest.calls[0].args[0]
expect(options.url).toMatch(/\/n1\/user/)
expect(IdentityStore.saveIdentity).toHaveBeenCalled()
const newIdent = IdentityStore.saveIdentity.calls[0].args[0]
@ -129,10 +130,10 @@ describe("IdentityStore", function identityStoreSpec() {
});
it("errors if the json is invalid", async () => {
spyOn(IdentityStore, "nylasIDRequest").andCallFake(() => {
spyOn(NylasAPIRequest, "makeRequest").andCallFake(() => {
return Promise.resolve({})
})
await IdentityStore._fetchIdentity();
await IdentityStore.fetchIdentity();
expect(NylasEnv.reportError).toHaveBeenCalled()
expect(IdentityStore.saveIdentity).not.toHaveBeenCalled()
});
@ -140,10 +141,10 @@ describe("IdentityStore", function identityStoreSpec() {
it("errors if the json doesn't match the ID", async () => {
const resp = Utils.deepClone(this.identityJSON);
resp.id = "THE WRONG ID"
spyOn(IdentityStore, "nylasIDRequest").andCallFake(() => {
spyOn(NylasAPIRequest, "makeRequest").andCallFake(() => {
return Promise.resolve(resp)
})
await IdentityStore._fetchIdentity();
await IdentityStore.fetchIdentity();
expect(NylasEnv.reportError).toHaveBeenCalled()
expect(IdentityStore.saveIdentity).not.toHaveBeenCalled()
});

View file

@ -3,7 +3,6 @@ Folder = require('../../src/flux/models/folder').default
Thread = require('../../src/flux/models/thread').default
Message = require('../../src/flux/models/message').default
Actions = require('../../src/flux/actions').default
NylasAPI = require('../../src/flux/nylas-api').default
Query = require('../../src/flux/models/query').default
DatabaseStore = require('../../src/flux/stores/database-store').default
ChangeFolderTask = require('../../src/flux/tasks/change-folder-task').default

View file

@ -3,7 +3,6 @@ Label = require('../../src/flux/models/label').default
Thread = require('../../src/flux/models/thread').default
Message = require('../../src/flux/models/message').default
Actions = require('../../src/flux/actions').default
NylasAPI = require('../../src/flux/nylas-api').default
DatabaseStore = require('../../src/flux/stores/database-store').default
ChangeLabelsTask = require('../../src/flux/tasks/change-labels-task').default
ChangeMailTask = require('../../src/flux/tasks/change-mail-task').default

View file

@ -8,8 +8,6 @@ import {
Contact,
Task,
SendDraftTask,
NylasAPI,
NylasAPIHelpers,
NylasAPIRequest,
SoundRegistry,
SyncbackMetadataTask,

View file

@ -3,12 +3,15 @@ ReactDOM = require 'react-dom'
{Utils,
RegExpUtils,
IdentityStore,
NylasAPIRequest,
SearchableComponentMaker,
SearchableComponentStore}= require 'nylas-exports'
SearchableComponentStore} = require 'nylas-exports'
IFrameSearcher = require('../searchable-components/iframe-searcher').default
url = require 'url'
_ = require "underscore"
{rootURLForServer} = NylasAPIRequest
###
Public: EventedIFrame is a thin wrapper around the DOM's standard `<iframe>` element.
You should always use EventedIFrame, because it provides important event hooks that
@ -168,8 +171,8 @@ class EventedIFrame extends React.Component
# If this is a link to our billing site, attempt single sign on instead of
# just following the link directly
if rawHref.startsWith(IdentityStore.URLRoot)
path = rawHref.split(IdentityStore.URLRoot).pop()
if rawHref.startsWith(rootURLForServer('identity'))
path = rawHref.split(rootURLForServer('identity')).pop()
IdentityStore.fetchSingleSignOnURL(path, {source: "SingleSignOnEmail"}).then (href) =>
NylasEnv.windowEventHandler.openLink(href: href, metaKey: e.metaKey)
return

View file

@ -1,4 +1,4 @@
import {React, Actions, NylasAPI, NylasAPIHelpers, APIError} from 'nylas-exports'
import {React, Actions, NylasAPIRequest, NylasAPIHelpers, APIError} from 'nylas-exports'
import {RetinaImg} from 'nylas-component-kit'
import classnames from 'classnames'
import _ from 'underscore'
@ -74,7 +74,7 @@ export default class MetadataComposerToggleButton extends React.Component {
NylasEnv.reportError(error);
} else if (error.statusCode === 400) {
NylasEnv.reportError(error);
} else if (NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {
} else if (NylasAPIRequest.TimeoutErrorCodes.includes(error.statusCode)) {
title = "Offline"
}

View file

@ -1,7 +1,7 @@
import _ from 'underscore';
import React from 'react';
import {LegacyEdgehillAPI} from "nylas-exports";
import {RetinaImg, Flexbox} from 'nylas-component-kit';
import {makeRequest} from '../flux/nylas-api-request';
export default class NewsletterSignup extends React.Component {
static displayName = 'NewsletterSignup';
@ -35,53 +35,54 @@ export default class NewsletterSignup extends React.Component {
this.setState(state);
}
_onGetStatus = (props = this.props) => {
_onGetStatus = async (props = this.props) => {
this._setState({status: 'Pending'});
LegacyEdgehillAPI.makeRequest({
method: 'GET',
path: this._path(props),
}).run().then((status) => {
try {
const status = await makeRequest({
server: 'identity',
method: 'GET',
path: this._path(props),
})
if (status === 'Never Subscribed') {
this._onSubscribe();
} else {
this._setState({status});
}
})
.catch(() => {
} catch (err) {
this._setState({status: "Error"});
})
}
}
_onSubscribe = () => {
_onSubscribe = async () => {
this._setState({status: 'Pending'});
LegacyEdgehillAPI.makeRequest({
method: 'POST',
path: this._path(),
}).run()
.then((status) => {
try {
const status = await makeRequest({
server: 'identity',
method: 'POST',
path: this._path(),
});
this._setState({status});
})
.catch(() => {
} catch (err) {
this._setState({status: "Error"});
})
}
}
_onUnsubscribe = () => {
this._setState({status: 'Pending'});
LegacyEdgehillAPI.makeRequest({
method: 'DELETE',
path: this._path(),
}).run()
.then((status) => {
try {
const status = makeRequest({
server: 'identity',
method: 'DELETE',
path: this._path(),
});
this._setState({status});
})
.catch(() => {
} catch (err) {
this._setState({status: "Error"});
})
}
}
_path(props = this.props) {
return `/newsletter-subscription/${encodeURIComponent(props.emailAddress)}?name=${encodeURIComponent(props.name)}`;
return `/api/newsletter-subscription/${encodeURIComponent(props.emailAddress)}?name=${encodeURIComponent(props.name)}`;
}
_renderControl() {

View file

@ -1,66 +0,0 @@
import AccountStore from './stores/account-store'
import IdentityStore from './stores/identity-store'
import NylasAPIRequest from './nylas-api-request';
// We're currently moving between services hosted on edgehill-api (written in
// Python) and services written in Node. Since we're doing this move progressively,
// we need to be able to use the two services at once. That's why we have two
// objects, EdgehillAPI (new API) and LegacyEdgehillAPI (old API).
class _EdgehillAPI {
constructor() {
NylasEnv.config.onDidChange('env', this._onConfigChanged);
this._onConfigChanged();
}
_onConfigChanged = () => {
const env = NylasEnv.config.get('env')
if (['development', 'local'].includes(env)) {
this.APIRoot = "http://n1-auth.lvh.me:5555";
} else if (env === 'staging') {
this.APIRoot = "https://n1-auth-staging.nylas.com";
} else {
this.APIRoot = "https://n1-auth.nylas.com";
}
}
accessTokenForAccountId(aid) {
return AccountStore.tokensForAccountId(aid).n1Cloud
}
makeRequest(options = {}) {
if (NylasEnv.getLoadSettings().isSpec) {
return {run: () => Promise.resolve()}
}
if (options.authWithNylasAPI) {
if (!IdentityStore.identity()) {
throw new Error('LegacyEdgehillAPI.makeRequest: Identity must be present to make a request that auths with Nylas API')
}
// The account doesn't matter for Edgehill server. We just need to
// ensure it's a valid account.
options.accountId = AccountStore.accounts()[0].id;
// The `NylasAPIRequest` object will grab the appropriate tokens.
delete options.auth;
delete options.authWithNylasAPI;
} else {
// A majority of Edgehill-server (aka auth) requests neither need
// (nor have) account or N1 ID tokens to provide.
// The existence of the options.auth object will prevent
// `NylasAPIRequest` from constructing them from existing tokens
options.auth = options.auth || {
user: '',
pass: '',
sendImmediately: true,
};
}
const req = new NylasAPIRequest({
api: this,
options,
});
return req;
}
}
const EdgehillAPI = new _EdgehillAPI();
export {EdgehillAPI, _EdgehillAPI};

View file

@ -1,18 +0,0 @@
// We use this class to access all the services we haven't migrated
// yet to the new codebase.
import {_EdgehillAPI} from './edgehill-api'
class LegacyEdgehillAPI extends _EdgehillAPI {
_onConfigChanged = () => {
const env = NylasEnv.config.get('env')
if (['development', 'local'].includes(env)) {
this.APIRoot = "http://localhost:5009";
} else if (env === 'staging') {
this.APIRoot = "https://edgehill-staging.nylas.com";
} else {
this.APIRoot = "https://edgehill.nylas.com";
}
}
}
export default new LegacyEdgehillAPI();

View file

@ -1,161 +1,238 @@
import Utils from './models/utils'
import Actions from './actions'
import {APIError} from './errors'
import IdentityStore from './stores/identity-store'
// A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT"
export const TimeoutErrorCodes = [0, 408, "ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ENETDOWN", "ENETUNREACH"]
export const PermanentErrorCodes = [400, 401, 402, 403, 404, 405, 429, 500, "ENOTFOUND", "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH"]
export const CanceledErrorCodes = [-123, "ECONNABORTED"]
export const SampleTemporaryErrorCode = 504
export default class NylasAPIRequest {
// server option
constructor({api, options}) {
const defaults = {
url: `${options.APIRoot || api.APIRoot}${options.path}`,
method: 'GET',
json: true,
timeout: 30000,
started: () => {},
}
export function rootURLForServer(server) {
const env = NylasEnv.config.get('env');
this.api = api;
this.options = Object.assign(defaults, options);
this.response = null
const bodyIsRequired = (this.options.method !== 'GET' && !this.options.formData);
if (bodyIsRequired) {
const fallback = this.options.json ? {} : '';
this.options.body = this.options.body || fallback;
}
if (!['development', 'local', 'staging', 'production'].includes(env)) {
throw new Error(`rootURLForServer: ${env} is not a valid environment.`);
}
async run() {
if (server === 'identity') {
return {
local: "http://localhost:5101",
development: "http://localhost:5101",
staging: "https://id-staging.nylas.com",
production: "https://id.nylas.com",
}[env];
}
if (server === 'accounts') {
return {
local: "http://localhost:5100",
development: "http://localhost:5100",
staging: "https://accounts-staging.nylas.com",
production: "https://accounts.nylas.com",
}[env];
}
throw new Error("rootURLForServer: You must provide a valid `server` value");
}
export async function makeRequest(options) {
try {
const root = rootURLForServer(options.server);
options.headers = options.headers || new Headers();
options.headers.set('Accept', 'application/json');
options.credentials = 'include';
if (!options.auth) {
if (options.server === 'identity') {
options.headers.set('Authorization', `Basic ${btoa(`${IdentityStore._identity.token}:`)}`)
}
}
if (options.path) {
options.url = `${root}${options.path}`;
}
if (options.body && !(options.body instanceof FormData)) {
options.headers.set('Content-Type', 'application/json');
options.body = JSON.stringify(options.body);
console.log(options.body);
}
const resp = await fetch(options.url, options);
if (!resp.ok) {
throw new APIError(resp.statusText);
}
return resp.json();
} catch (err) {
const error = new APIError(err) || new APIError(`${options.url} returned ${err.message}.`)
NylasEnv.reportError(error);
return null;
// TODO BG: Promise.reject();
// if (NylasEnv.getLoadSettings().isSpec) return Promise.resolve([]);
// try {
// this.options.auth = this.options.auth || this._defaultAuth();
// return await this._asyncRequest(this.options)
// } catch (error) {
// let apiError = error
// if (!(apiError instanceof APIError)) {
// apiError = new APIError({error: apiError, statusCode: 500})
// }
// this._notifyOfAPIError(apiError)
// throw apiError
// }
}
/**
* An async wrapper around `request`. We reject on any non 2xx codes or
* other errors.
*
* Resolves to the JSON body or rejects with an APIError object.
*/
async _asyncRequest(options = {}) {
return new Promise((resolve, reject) => {
// Blob requests can potentially contain megabytes of binary data.
// it doesn't make sense to send them through the action bridge.
const req = request(options, (error, response, body) => {
this.response = response;
const statusCode = (response || {}).statusCode;
if (statusCode >= 200 && statusCode <= 299) {
return resolve(body)
}
const apiError = new APIError({
body: body,
error: error,
response: response,
statusCode: statusCode,
requestOptions: options,
});
return reject(apiError)
});
req.on('abort', () => {
// Use a status code of 0 because we don't want to report the error when
// we manually abort the request
const statusCode = 0
const abortedError = new APIError({
statusCode,
body: 'Request aborted by client',
});
reject(abortedError);
});
req.on('aborted', () => {
const statusCode = "ECONNABORTED"
const abortedError = new APIError({
statusCode,
body: 'Request aborted by server',
});
reject(abortedError);
});
options.started(req);
})
}
async _notifyOfAPIError(apiError) {
const {statusCode} = apiError
// TODO move this check into NylasEnv.reportError()?
if (apiError.shouldReportError()) {
const msg = apiError.message || `Unknown Error: ${apiError}`
const fingerprint = ["{{ default }}", "api error", this.options.url, apiError.statusCode, msg];
NylasEnv.reportError(apiError, {fingerprint,
rateLimit: {
ratePerHour: 30,
key: `APIError:${this.options.url}:${statusCode}:${msg}`,
},
});
apiError.reported = true
}
if ([401, 403].includes(statusCode)) {
Actions.apiAuthError(apiError, this.options, this.api.constructor.name)
}
}
/**
* Generates the basic auth username from the account token and the
* basic auth password from the NylasID token.
*
* This asserts if any of these pieces are missing and throws an
* APIError object.
*/
_defaultAuth() {
try {
if (!this.options.accountId) {
throw new Error("Cannot make Nylas request without specifying `auth` or an `accountId`.");
}
const identity = IdentityStore.identity();
if (!identity || !identity.token) {
// const clickedIndex = remote.dialog.showMessageBox({
// type: 'error',
// message: 'Your NylasID is invalid. Please log out then log back in.',
// detail: `Actions like sending and receiving mail require this token. Please log back into your Nylas ID to restore it—your email accounts will not be removed in this process.`,
// buttons: ['Log out'],
// })
// if (clickedIndex === 0) {
// Actions.logoutNylasIdentity()
// }
throw new Error("No Identity")
}
const accountToken = this.api.accessTokenForAccountId(this.options.accountId);
if (!accountToken) {
throw new Error(`Auth token missing for account`);
}
return {
user: accountToken,
pass: identity.token,
sendImmediately: true,
};
} catch (error) {
throw new APIError({error, statusCode: 400});
}
}
}
export default {
TimeoutErrorCodes,
PermanentErrorCodes,
CanceledErrorCodes,
SampleTemporaryErrorCode,
rootURLForServer,
makeRequest,
}
// export default class NylasAPIRequest {
// constructor({api, options}) {
// const defaults = {
// url: `${options.APIRoot || api.APIRoot}${options.path}`,
// method: 'GET',
// json: true,
// timeout: 30000,
// started: () => {},
// }
// this.api = api;
// this.options = Object.assign(defaults, options);
// this.response = null
// const bodyIsRequired = (this.options.method !== 'GET' && !this.options.formData);
// if (bodyIsRequired) {
// const fallback = this.options.json ? {} : '';
// this.options.body = this.options.body || fallback;
// }
// }
// async run() {
// return null;
// // TODO BG: Promise.reject();
// // if (NylasEnv.getLoadSettings().isSpec) return Promise.resolve([]);
// // try {
// // this.options.auth = this.options.auth || this._defaultAuth();
// // return await this._asyncRequest(this.options)
// // } catch (error) {
// // let apiError = error
// // if (!(apiError instanceof APIError)) {
// // apiError = new APIError({error: apiError, statusCode: 500})
// // }
// // this._notifyOfAPIError(apiError)
// // throw apiError
// // }
// }
// /**
// * An async wrapper around `request`. We reject on any non 2xx codes or
// * other errors.
// *
// * Resolves to the JSON body or rejects with an APIError object.
// */
// async _asyncRequest(options = {}) {
// return new Promise((resolve, reject) => {
// // Blob requests can potentially contain megabytes of binary data.
// // it doesn't make sense to send them through the action bridge.
// const req = request(options, (error, response, body) => {
// this.response = response;
// const statusCode = (response || {}).statusCode;
// if (statusCode >= 200 && statusCode <= 299) {
// return resolve(body)
// }
// const apiError = new APIError({
// body: body,
// error: error,
// response: response,
// statusCode: statusCode,
// requestOptions: options,
// });
// return reject(apiError)
// });
// req.on('abort', () => {
// // Use a status code of 0 because we don't want to report the error when
// // we manually abort the request
// const statusCode = 0
// const abortedError = new APIError({
// statusCode,
// body: 'Request aborted by client',
// });
// reject(abortedError);
// });
// req.on('aborted', () => {
// const statusCode = "ECONNABORTED"
// const abortedError = new APIError({
// statusCode,
// body: 'Request aborted by server',
// });
// reject(abortedError);
// });
// options.started(req);
// })
// }
// async _notifyOfAPIError(apiError) {
// const {statusCode} = apiError
// // TODO move this check into NylasEnv.reportError()?
// if (apiError.shouldReportError()) {
// const msg = apiError.message || `Unknown Error: ${apiError}`
// const fingerprint = ["{{ default }}", "api error", this.options.url, apiError.statusCode, msg];
// NylasEnv.reportError(apiError, {fingerprint,
// rateLimit: {
// ratePerHour: 30,
// key: `APIError:${this.options.url}:${statusCode}:${msg}`,
// },
// });
// apiError.reported = true
// }
// if ([401, 403].includes(statusCode)) {
// Actions.apiAuthError(apiError, this.options, this.api.constructor.name)
// }
// }
// /**
// * Generates the basic auth username from the account token and the
// * basic auth password from the NylasID token.
// *
// * This asserts if any of these pieces are missing and throws an
// * APIError object.
// */
// _defaultAuth() {
// try {
// if (!this.options.accountId) {
// throw new Error("Cannot make Nylas request without specifying `auth` or an `accountId`.");
// }
// const identity = IdentityStore.identity();
// if (!identity || !identity.token) {
// // const clickedIndex = remote.dialog.showMessageBox({
// // type: 'error',
// // message: 'Your NylasID is invalid. Please log out then log back in.',
// // detail: `Actions like sending and receiving mail require this token. Please log back into your Nylas ID to restore it—your email accounts will not be removed in this process.`,
// // buttons: ['Log out'],
// // })
// // if (clickedIndex === 0) {
// // Actions.logoutNylasIdentity()
// // }
// throw new Error("No Identity")
// }
// const accountToken = this.api.accessTokenForAccountId(this.options.accountId);
// if (!accountToken) {
// throw new Error(`Auth token missing for account`);
// }
// return {
// user: accountToken,
// pass: identity.token,
// sendImmediately: true,
// };
// } catch (error) {
// throw new APIError({error, statusCode: 400});
// }
// }
// }

View file

@ -1,28 +0,0 @@
import AccountStore from './stores/account-store'
// A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT"
const TimeoutErrorCodes = [0, 408, "ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ENETDOWN", "ENETUNREACH"]
const PermanentErrorCodes = [400, 401, 402, 403, 404, 405, 429, 500, "ENOTFOUND", "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH"]
const CanceledErrorCodes = [-123, "ECONNABORTED"]
const SampleTemporaryErrorCode = 504
class NylasAPI {
constructor() {
let port = 2578;
if (NylasEnv.inDevMode()) port = 1337;
this.APIRoot = `http://localhost:${port}`
this.TimeoutErrorCodes = TimeoutErrorCodes
this.PermanentErrorCodes = PermanentErrorCodes
this.CanceledErrorCodes = CanceledErrorCodes
this.SampleTemporaryErrorCode = SampleTemporaryErrorCode
}
accessTokenForAccountId(aid) {
return AccountStore.tokensForAccountId(aid).localSync
}
}
export default new NylasAPI()

View file

@ -5,6 +5,7 @@ import url from 'url'
import Utils from '../models/utils';
import Actions from '../actions';
import KeyManager from '../../key-manager';
import {makeRequest, rootURLForServer} from '../nylas-api-request';
// Note this key name is used when migrating to Nylas Pro accounts from old N1.
const KEYCHAIN_NAME = 'Nylas Account';
@ -25,9 +26,6 @@ class IdentityStore extends NylasStore {
return
}
NylasEnv.config.onDidChange('env', this._onEnvChanged);
this._onEnvChanged();
NylasEnv.config.onDidChange('nylasid', this._onIdentityChanged);
this._onIdentityChanged();
@ -106,17 +104,6 @@ class IdentityStore extends NylasStore {
remote.app.quit()
}
_onEnvChanged = () => {
const env = NylasEnv.config.get('env')
if (['development', 'local'].includes(env)) {
this.URLRoot = "http://localhost:5101";
} else if (env === 'staging') {
this.URLRoot = "https://billing-staging.nylas.com";
} else {
this.URLRoot = "https://billing.nylas.com";
}
}
/**
* This passes utm_source, utm_campaign, and utm_content params to the
* N1 billing site. Please reference:
@ -151,16 +138,17 @@ class IdentityStore extends NylasStore {
body.append('next_path', pathWithUtm);
try {
const json = await this.nylasIDRequest({
const json = await makeRequest({
server: 'identity',
path: '/api/login-link',
qs: qs,
body: body,
timeout: 1500,
method: 'POST',
});
return `${this.URLRoot}${json.path}`;
return `${rootURLForServer('identity')}${json.path}`;
} catch (err) {
return `${this.URLRoot}${path}`;
return `${rootURLForServer('identity')}${path}`;
}
}
@ -168,7 +156,13 @@ class IdentityStore extends NylasStore {
if (!this._identity || !this._identity.token) {
return Promise.resolve();
}
const json = await this.nylasIDRequest({path: '/api/me', method: 'GET'});
const json = await makeRequest({
server: 'identity',
path: '/api/me',
method: 'GET',
});
if (!json || !json.id || json.id !== this._identity.id) {
console.error(json)
NylasEnv.reportError(new Error("Remote Identity returned invalid json"), json || {})
@ -177,26 +171,6 @@ class IdentityStore extends NylasStore {
const nextIdentity = Object.assign({}, this._identity, json);
return this.saveIdentity(nextIdentity);
}
async nylasIDRequest(options) {
try {
if (options.path) {
options.url = `${this.URLRoot}${options.path}`;
}
options.credentials = 'include';
options.headers = new Headers();
options.headers.set('Authorization', `Basic ${btoa(`${this._identity.token}:`)}`)
const resp = await fetch(options.url, options);
if (!resp.ok) {
throw new Error(resp.statusText);
}
return resp.json();
} catch (err) {
const error = err || new Error(`IdentityStore.nylasIDRequest: ${options.url} ${err.message}.`)
NylasEnv.reportError(error)
return null
}
}
}
export default new IdentityStore()

View file

@ -1,7 +1,6 @@
import NylasStore from 'nylas-store';
import {remote} from 'electron';
import {LegacyEdgehillAPI} from 'nylas-exports';
import {makeRequest} from '../nylas-api-request';
const autoUpdater = remote.getGlobal('application').autoUpdateManager;
const preferredChannel = autoUpdater.preferredChannel;
@ -29,41 +28,44 @@ class UpdateChannelStore extends NylasStore {
return this._available;
}
refreshChannel() {
async refreshChannel() {
// TODO BG
return;
LegacyEdgehillAPI.makeRequest({
method: 'GET',
path: `/update-channel`,
qs: Object.assign({preferredChannel: preferredChannel}, autoUpdater.parameters()),
json: true,
}).run()
.then(({current, available} = {}) => {
try {
const {current, available} = await makeRequest({
server: 'identity',
method: 'GET',
path: `/api/update-channel`,
qs: Object.assign({preferredChannel: preferredChannel}, autoUpdater.parameters()),
json: true,
});
this._current = current || {name: "Edgehill API Not Available"};
this._available = available || [];
this.trigger();
});
return null;
} catch (err) {
// silent
}
return;
}
setChannel(channelName) {
LegacyEdgehillAPI.makeRequest({
method: 'POST',
path: `/update-channel`,
qs: Object.assign({
channel: channelName,
preferredChannel: preferredChannel,
}, autoUpdater.parameters()),
json: true,
}).run()
.then(({current, available} = {}) => {
async setChannel(channelName) {
try {
const {current, available} = await makeRequest({
server: 'identity',
method: 'POST',
path: `/api/update-channel`,
qs: Object.assign({
channel: channelName,
preferredChannel: preferredChannel,
}, autoUpdater.parameters()),
json: true,
});
this._current = current || {name: "Edgehill API Not Available"};
this._available = available || [];
this.trigger();
}).catch((err) => {
} catch (err) {
NylasEnv.showErrorDialog(err.toString())
this.trigger();
});
}
return null;
}
}

View file

@ -1,5 +1,5 @@
import Task from './task';
import NylasAPI from '../nylas-api'
import {makeRequest, PermanentErrorCodes} from '../nylas-api-request'
import {APIError} from '../errors'
import IdentityStore from '../stores/identity-store'
@ -23,20 +23,20 @@ export default class SendFeatureUsageEventTask extends Task {
}
async performRemote() {
const body = new FormData();
body.append('feature_name', this.featureName);
const options = {
method: 'POST',
path: `/api/feature_usage_event`,
body: body,
};
try {
const updatedIdentity = await IdentityStore.nylasIDRequest(options);
const updatedIdentity = await makeRequest({
server: 'identity',
method: 'POST',
path: `/api/feature_usage_event`,
body: {
feature_name: this.featureName,
},
});
await IdentityStore.saveIdentity(updatedIdentity);
return Promise.resolve(Task.Status.Success)
return Task.Status.Success;
} catch (err) {
if (err instanceof APIError) {
if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
if (PermanentErrorCodes.includes(err.statusCode)) {
this.revert()
return Promise.resolve([Task.Status.Failed, err])
}

View file

@ -1,7 +1,7 @@
import SyncbackModelTask from './syncback-model-task'
import DatabaseObjectRegistry from '../../registries/database-object-registry'
import N1CloudAPI from '../../n1-cloud-api'
import NylasAPIRequest from '../nylas-api-request'
import {makeRequest} from '../nylas-api-request'
export default class SyncbackMetadataTask extends SyncbackModelTask {
@ -38,10 +38,10 @@ export default class SyncbackMetadataTask extends SyncbackModelTask {
messageIds: messageIds,
},
};
return new NylasAPIRequest({
return makeRequest({
api: N1CloudAPI,
options,
}).run()
});
}
applyRemoteChangesToModel = (model, {version}) => {

View file

@ -3,7 +3,7 @@ import _ from 'underscore';
import Model from '../models/model';
import Attributes from '../attributes';
import {generateTempId} from '../models/utils';
import {PermanentErrorCodes} from '../nylas-api';
import {PermanentErrorCodes} from '../nylas-api-request';
import {APIError} from '../errors';
const Status = {

View file

@ -51,10 +51,7 @@ const lazyLoadAndRegisterTask = (klassName, path) => {
lazyLoad(`Actions`, 'flux/actions');
// API Endpoints
lazyLoad(`NylasAPI`, 'flux/nylas-api');
lazyLoad(`N1CloudAPI`, 'n1-cloud-api');
lazyLoad(`EdgehillAPI`, 'flux/edgehill-api');
lazyLoad(`LegacyEdgehillAPI`, 'flux/legacy-edgehill-api');
lazyLoad(`NylasAPIHelpers`, 'flux/nylas-api-helpers');
lazyLoad(`NylasAPIRequest`, 'flux/nylas-api-request');
lazyLoad(`MailsyncProcess`, 'mailsync-process');