Remove legacy sync (#3028)

This commit is contained in:
David Marby
2021-01-27 13:01:53 +01:00
committed by GitHub
parent b4862d8270
commit 68949619fe
19 changed files with 8 additions and 2472 deletions

View File

@@ -55,9 +55,6 @@ type BaseSettings = {
useBulkHeaderEditor: boolean,
useBulkParametersEditor: boolean,
validateSSL: boolean,
// Feature flags
enableSyncBeta: boolean,
};
export type Settings = BaseModel & BaseSettings;
@@ -111,9 +108,6 @@ export function init(): BaseSettings {
useBulkHeaderEditor: false,
useBulkParametersEditor: false,
validateSSL: true,
// Feature flags
enableSyncBeta: false,
};
}

View File

@@ -1,557 +0,0 @@
import * as sync from '../index';
import * as models from '../../models';
import * as db from '../../common/database';
import * as syncStorage from '../storage';
import * as network from '../network';
import { globalBeforeEach } from '../../__jest__/before-each';
import * as session from '../../account/session';
import * as crypt from '../../account/crypt';
describe('Test push/pull behaviour', () => {
beforeEach(async () => {
await globalBeforeEach();
// Reset some things
sync._testReset();
await _setSessionData();
await _setupSessionMocks();
// Init sync and storage
const config = { inMemoryOnly: true, autoload: false, filename: null };
await syncStorage.initDB(config, true);
// Add some data
await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
await models.workspace.create({ _id: 'wrk_2', name: 'Workspace 2' });
await models.request.create({
_id: 'req_1',
name: 'Request 1',
parentId: 'wrk_1',
});
await models.request.create({
_id: 'req_2',
name: 'Request 2',
parentId: 'wrk_2',
});
// Create resources, resource groups, and configs
const workspaces = await models.workspace.all();
const requests = await models.request.all();
for (const d of [...workspaces, ...requests]) {
await sync.getOrCreateResourceForDoc(d);
}
});
it('Pushes sync mode with and without resource group id', async () => {
const request = await models.request.getById('req_1');
const request2 = await models.request.getById('req_2');
const resourceRequest = await syncStorage.getResourceByDocId(request._id);
const resourceRequest2 = await syncStorage.getResourceByDocId(request2._id);
// Set up sync modes
await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
syncMode: syncStorage.SYNC_MODE_ON,
});
await sync.createOrUpdateConfig(resourceRequest2.resourceGroupId, {
syncMode: syncStorage.SYNC_MODE_UNSET,
});
await sync.push(); // Push only active configs
await sync.push(resourceRequest.resourceGroupId); // Force push rg_1
await sync.push(resourceRequest2.resourceGroupId); // Force push rg_2
expect(network.syncPush.mock.calls.length).toBe(3);
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
expect(network.syncPush.mock.calls[1][0].length).toBe(2);
expect(network.syncPush.mock.calls[1][0][0].id).toBe('wrk_1');
expect(network.syncPush.mock.calls[1][0][1].id).toBe('req_1');
expect(network.syncPush.mock.calls[2][0].length).toBe(2);
expect(network.syncPush.mock.calls[2][0][0].id).toBe('wrk_2');
expect(network.syncPush.mock.calls[2][0][1].id).toBe('req_2');
});
it('Updates dirty flag for push response', async () => {
const request = await models.request.getById('req_1');
const resourceRequest = await syncStorage.getResourceByDocId(request._id);
await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
syncMode: syncStorage.SYNC_MODE_ON,
});
network.syncPush.mockReturnValueOnce({
updated: [],
created: [{ id: request._id, version: 'new-version' }],
removed: [],
conflicts: [],
});
const resourceBefore = await syncStorage.getResourceByDocId(request._id);
await sync.push(resourceRequest.resourceGroupId);
const resourceAfter = await syncStorage.getResourceByDocId(request._id);
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
expect(resourceBefore.dirty).toBe(true);
expect(resourceAfter.dirty).toBe(false);
});
it('Updates resources for pull response', async () => {
const request = await models.request.getById('req_1');
const request2 = await models.request.getById('req_2');
const requestNew = Object.assign({}, request, {
_id: 'req_new',
name: 'New Request',
});
const resourceBefore = await syncStorage.getResourceByDocId(request._id);
const resource2Before = await syncStorage.getResourceByDocId(requestNew._id);
await sync.createOrUpdateConfig(resourceBefore.resourceGroupId, {
syncMode: syncStorage.SYNC_MODE_ON,
});
const updatedRequest = Object.assign({}, request, {
name: 'Request Updated',
});
const updatedResource = Object.assign({}, resourceBefore, {
version: 'ver1',
encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, updatedRequest),
});
const createdResourceNew = Object.assign({}, resourceBefore, {
id: requestNew._id,
resourceGroupId: 'rg_1',
encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, requestNew),
});
network.syncPull.mockReturnValueOnce({
updatedResources: [updatedResource],
createdResources: [createdResourceNew],
idsToPush: [],
idsToRemove: ['req_2'],
});
// Pull and get docs/resources
await sync.pull(resourceBefore.resourceGroupId);
const requestAfter = await models.request.getById(request._id);
const request2After = await models.request.getById(request2._id);
const requestNewAfter = await models.request.getById('req_new');
const resourceAfter = await syncStorage.getResourceByDocId(
request._id,
resourceBefore.resourceGroupId,
);
const resource2After = await syncStorage.getResourceByDocId(request2._id);
const resourceNewAfter = await syncStorage.getResourceByDocId(requestNewAfter._id);
// Assert
expect(resourceBefore.version).toBe('__NO_VERSION__');
expect(resourceAfter.version).toBe(updatedResource.version);
expect(resourceBefore.dirty).toBe(true);
expect(resource2Before).toBe(null);
expect(resourceAfter.dirty).toBe(false);
expect(resource2After.removed).toBe(true);
expect(requestAfter.name).toBe('Request Updated');
expect(request2After).toBe(null);
expect(resourceNewAfter).not.toBe(null);
});
it('Conflict: local version wins on modified before', async () => {
const requestClient = await models.request.getById('req_1');
const requestServer = Object.assign({}, requestClient, {
name: 'Server Request',
});
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
const resourceConflict = Object.assign({}, resourceRequest, {
version: 'ver-2',
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
lastEdited: resourceRequest.lastEdited - 1000, // Same edited time
});
network.syncPush.mockReturnValueOnce({
updated: [],
created: [],
removed: [],
conflicts: [resourceConflict],
});
await sync.push(resourceRequest.resourceGroupId);
const resourceAfter = await syncStorage.getResourceByDocId(
requestClient._id,
resourceRequest.resourceGroupId,
);
const requestAfter = await models.request.getById(requestClient._id);
// Assert
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
// Even when local wins, local resource gets the remove resource version
expect(resourceAfter.version).toBe(resourceConflict.version);
// Local resource gets marked as dirty so it's pushed right away
expect(resourceAfter.dirty).toBe(true);
// Local db should not be changed since the local won
expect(requestAfter).toEqual(requestClient);
});
it('Conflict: local version wins on modified tie', async () => {
const requestClient = await models.request.getById('req_1');
const requestServer = Object.assign({}, requestClient, {
name: 'Server Request',
});
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
const resourceConflict = Object.assign({}, resourceRequest, {
version: 'ver-2',
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
lastEdited: resourceRequest.lastEdited, // Same edited time
});
network.syncPush.mockReturnValueOnce({
updated: [],
created: [],
removed: [],
conflicts: [resourceConflict],
});
await sync.push(resourceRequest.resourceGroupId);
const resourceAfter = await syncStorage.getResourceByDocId(
requestClient._id,
resourceRequest.resourceGroupId,
);
const requestAfter = await models.request.getById(requestClient._id);
// Assert
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
// Even when local wins, local resource gets the remove resource version
expect(resourceAfter.version).toBe(resourceConflict.version);
// Local resource gets marked as dirty so it's pushed right away
expect(resourceAfter.dirty).toBe(true);
// Local db should not be changed since the local won
expect(requestAfter).toEqual(requestClient);
});
it('Conflict: server version wins if modified after', async () => {
const requestClient = await models.request.getById('req_1');
const requestServer = Object.assign({}, requestClient, {
name: 'Server Request',
});
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
const resourceConflict = Object.assign({}, resourceRequest, {
version: 'ver-2',
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
lastEdited: resourceRequest.lastEdited + 1000,
});
network.syncPush.mockReturnValueOnce({
updated: [],
created: [],
removed: [],
conflicts: [resourceConflict],
});
await sync.push(resourceRequest.resourceGroupId);
const resourceAfter = await syncStorage.getResourceByDocId(
requestClient._id,
resourceRequest.resourceGroupId,
);
const requestAfter = await models.request.getById(requestClient._id);
// Assert
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
expect(resourceAfter.lastEdited).toBeGreaterThan(resourceRequest.lastEdited);
expect(resourceAfter.version).toBe(resourceConflict.version);
// Local resource gets marked as dirty so it's pushed right away
expect(resourceAfter.dirty).toBe(false);
expect(requestAfter.name).toBe('Server Request');
expect(requestAfter.modified).toBe(requestServer.modified);
});
});
describe('Integration tests for creating Resources and pushing', () => {
beforeEach(async () => {
await globalBeforeEach();
// Reset some things
await _setSessionData();
sync._testReset();
// Mock some things
await _setupSessionMocks();
jest.useFakeTimers();
// Init storage
const config = { inMemoryOnly: true, autoload: false, filename: null };
await syncStorage.initDB(config, true);
// Add some data
await models.workspace.create({
_id: 'wrk_empty',
name: 'Workspace Empty',
});
await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
await models.request.create({
_id: 'req_1',
name: 'Request 1',
parentId: 'wrk_1',
});
await models.request.create({
_id: 'req_2',
name: 'Request 2',
parentId: 'wrk_1',
});
await models.request.create({
_id: 'req_3',
name: 'Request 3',
parentId: 'wrk_1',
});
await models.environment.create({
_id: 'env_2',
name: 'Env Prv',
parentId: 'wrk_1',
isPrivate: true,
});
// Flush changes just to be sure they won't affect our tests
await db.flushChanges();
await sync.writePendingChanges();
// Assert that all our new models were created
expect((await models.workspace.all()).length).toBe(2);
expect((await models.request.all()).length).toBe(3);
expect((await models.environment.all()).length).toBe(1);
expect((await models.cookieJar.all()).length).toBe(0);
// Assert that initializing sync will create the initial resources
expect((await syncStorage.allConfigs()).length).toBe(0);
expect((await syncStorage.allResources()).length).toBe(0);
const promise = sync.init();
jest.runOnlyPendingTimers();
await promise;
expect((await syncStorage.allConfigs()).length).toBe(2);
expect((await syncStorage.allResources()).length).toBe(5);
// Mark all configs as auto sync
const configs = await syncStorage.allConfigs();
for (const config of configs) {
await syncStorage.updateConfig(config, {
syncMode: syncStorage.SYNC_MODE_ON,
});
}
// Do initial push
await sync.push();
// Reset mocks once again before tests
await _setupSessionMocks();
});
it('Resources created on DB change', async () => {
// Fetch the workspace and create a new request
await db.bufferChanges();
await models.request.create({
_id: 'req_t',
url: 'https://google.com',
parentId: 'wrk_1',
});
await db.flushChanges();
await sync.writePendingChanges();
await sync.push();
// Push changes and get resource
const resource = await syncStorage.getResourceByDocId('req_t');
// Assert
expect((await syncStorage.allConfigs()).length).toBe(2);
expect((await syncStorage.allResources()).length).toBe(6);
expect(_decryptResource(resource).url).toBe('https://google.com');
expect(resource.removed).toBe(false);
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(6);
expect(network.syncPull.mock.calls).toEqual([]);
});
it('Resources revived on DB change', async () => {
// Fetch the workspace and create a new request
await db.bufferChanges();
const request = await models.request.create({
_id: 'req_t',
name: 'Original Request',
parentId: 'wrk_1',
});
await db.flushChanges();
await sync.writePendingChanges();
await sync.push();
// Mark resource as removed
const originalResource = await syncStorage.getResourceByDocId('req_t');
const updatedResource = await syncStorage.updateResource(originalResource, {
removed: true,
});
// Update it and push it again
await db.bufferChanges();
await models.request.update(request, { name: 'New Name' });
await db.flushChanges();
await sync.writePendingChanges();
await sync.push();
const finalResource = await syncStorage.getResourceByDocId('req_t');
// Assert
expect(originalResource.removed).toBe(false);
expect(updatedResource.removed).toBe(true);
expect(finalResource.removed).toBe(false);
});
it('Resources update on DB change', async () => {
// Create, update a request, and fetch it's resource
const request = await models.request.getById('req_1');
const resource = await syncStorage.getResourceByDocId(request._id);
await db.bufferChanges();
const updatedRequest = await models.request.update(request, {
name: 'New Name',
});
// Drain and fetch new resource
await db.flushChanges();
await sync.writePendingChanges();
await sync.push();
const updatedResource = await syncStorage.getResourceByDocId(request._id);
// Assert
expect(request.name).toBe('Request 1');
expect(_decryptResource(resource).name).toBe('Request 1');
expect(updatedRequest.name).toBe('New Name');
expect(_decryptResource(updatedResource).name).toBe('New Name');
expect(resource.removed).toBe(false);
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
expect(network.syncPull.mock.calls).toEqual([]);
});
it('Resources removed on DB change', async () => {
// Create, update a request, and fetch it's resource
const request = await models.request.getById('req_1');
const resource = await syncStorage.getResourceByDocId(request._id);
await db.bufferChanges();
await models.request.remove(request);
// Drain and fetch new resource
await db.flushChanges();
await sync.writePendingChanges();
await sync.push();
const updatedResource = await syncStorage.getResourceByDocId(request._id);
// Assert
expect(resource.removed).toBe(false);
expect(updatedResource.removed).toBe(true);
expect(network.syncPush.mock.calls.length).toBe(1);
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
expect(network.syncPull.mock.calls).toEqual([]);
});
});
// ~~~~~~~ //
// Helpers //
// ~~~~~~~ //
function _decryptResource(resource) {
const message = JSON.parse(resource.encContent);
const fakeKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const docJSON = crypt.decryptAES(fakeKey, message);
return JSON.parse(docJSON);
}
async function _setSessionData() {
const symmetricKey = {
alg: 'A256GCM',
ext: true,
k: '3-QU2OcQcpSyFIoL8idgclbImP3M8Y2d0oVAca3Vl4g',
key_ops: ['encrypt', 'decrypt'],
kty: 'oct',
};
const publicKey = {
alg: 'RSA-OAEP-256',
e: 'AQAB',
ext: true,
key_ops: ['encrypt'],
kty: 'RSA',
n: 'aaaa',
};
const { privateKey } = await crypt.generateKeyPairJWK();
const encPrivateKey = {
ad: '',
d: Buffer.from(JSON.stringify(privateKey)).toString('hex'),
iv: '968f1d810efdaec58f9e313e',
t: '0e87a2e57a198ca79cb99585fe9c244a',
};
// Setup mocks and stuff
session.setSessionData(
'ses_123',
'acct_123',
'Tammy',
'Tester',
'greg.schier@konghq.com',
symmetricKey,
publicKey,
encPrivateKey,
);
}
async function _setupSessionMocks() {
const resourceGroups = {};
network.syncCreateResourceGroup = jest.fn((parentId, name, _) => {
const id = `rg_${Object.keys(resourceGroups).length + 1}`;
// Generate a public key and use a symmetric equal to it's Id for
// convenience
const publicKey = session.getPublicKey();
const symmetricKeyStr = JSON.stringify({ k: id });
const encSymmetricKey = crypt.encryptRSAWithJWK(publicKey, symmetricKeyStr);
// Store the resource group and return it
resourceGroups[id] = Object.assign(
{},
{ id, encSymmetricKey },
{
parentResourceId: parentId,
name: name,
encSymmetricKey: encSymmetricKey,
},
);
return resourceGroups[id];
});
network.syncGetResourceGroup = jest.fn(id => {
if (resourceGroups[id]) {
return resourceGroups[id];
}
const err = new Error(`Not Found for ${id}`);
err.statusCode = 404;
throw err;
});
network.syncPull = jest.fn(body => ({
updatedResources: [],
createdResources: [],
idsToPush: [],
idsToRemove: [],
}));
network.syncPush = jest.fn(body => ({
conflicts: [],
updated: [],
created: [],
removed: [],
}));
}

View File

@@ -1,870 +0,0 @@
import * as db from '../common/database';
import * as models from '../models';
import * as store from './storage';
import * as misc from '../common/misc';
import Logger from './logger';
import * as zlib from 'zlib';
import {
syncCreateResourceGroup,
syncFixDupes,
syncGetResourceGroup,
syncPull,
syncPush,
syncResetData,
} from './network';
import * as crypt from '../account/crypt';
import * as session from '../account/session';
export const START_DELAY = 1e3;
export const PULL_PERIOD = 15e3;
export const WRITE_PERIOD = 1e3;
const WHITE_LIST = {
[models.workspace.type]: true,
[models.request.type]: true,
[models.requestGroup.type]: true,
[models.environment.type]: true,
[models.unitTest.type]: true,
[models.unitTestSuite.type]: true,
// These can be overridden in sync config
[models.cookieJar.type]: true,
[models.clientCertificate.type]: true,
};
export const logger = new Logger();
// TODO: Move this stuff somewhere else
const NO_VERSION = '__NO_VERSION__';
const resourceGroupSymmetricKeysCache = {};
let _pullChangesInterval = null;
let _writeChangesInterval = null;
let _pendingDBChanges = {};
let _isInitialized = false;
// Used to mark whether or not the new sync system is enabled
let _disabledForSession = false;
export function disableForSession() {
_disabledForSession = true;
}
export async function init() {
if (_disabledForSession) {
logger.debug('Legacy sync is disabled for current session');
return;
}
if (_isInitialized) {
logger.debug('Already enabled');
return;
}
// NOTE: This is at the top to prevent race conditions
_isInitialized = true;
db.onChange(async changes => {
// To help prevent bugs, put Workspaces first
const sortedChanges = changes.sort(([event, doc, fromSync]) =>
doc.type === models.workspace.type ? 1 : -1,
);
for (const [event, doc, fromSync] of sortedChanges) {
const notOnWhitelist = !WHITE_LIST[doc.type];
const notLoggedIn = !session.isLoggedIn();
if (doc.isPrivate) {
logger.debug(`Skip private doc change ${doc._id}`);
continue;
}
if (notLoggedIn || notOnWhitelist || fromSync) {
continue;
}
const key = `${event}:${doc._id}`;
_pendingDBChanges[key] = [event, doc, Date.now()];
}
});
await misc.delay(START_DELAY);
await push();
await pull();
let nextSyncTime = 0;
let isSyncing = false;
_pullChangesInterval = setInterval(async () => {
if (isSyncing) {
return;
}
if (Date.now() < nextSyncTime) {
return;
}
// Mark that we are currently executing a sync op
isSyncing = true;
const syncStartTime = Date.now();
let extraDelay = 0;
try {
await push();
await pull();
} catch (err) {
logger.error('Sync failed with', err);
extraDelay += PULL_PERIOD;
}
const totalSyncTime = Date.now() - syncStartTime;
// Add sync duration to give the server some room if it's being slow.
// Also, multiply it by a random value so everyone doesn't sync up
extraDelay += totalSyncTime * (Math.random() * 2);
nextSyncTime = Date.now() + PULL_PERIOD + extraDelay;
isSyncing = false;
}, PULL_PERIOD / 5);
_writeChangesInterval = setInterval(writePendingChanges, WRITE_PERIOD);
logger.debug('Initialized');
}
// Used only during tests!
export function _testReset() {
_isInitialized = false;
clearInterval(_pullChangesInterval);
clearInterval(_writeChangesInterval);
}
/**
* Non-blocking function to perform initial sync for an account. This will pull
* all remote resources (if they exist) before initializing sync.
*/
export function doInitialSync() {
process.nextTick(async () => {
// First, pull down all remote resources, without first creating new ones.
// This makes sure that the first sync won't create resources locally, when
// they already exist on the server.
await pull(null, false);
// Make sure sync is on (start the timers)
await init();
});
}
/**
* This is a function to clean up Workspaces that might have had more than one
* ResourceGroup created for them. This function should be called on init (or maybe
* even periodically) and can be removed once the bug stops persisting.
*/
export async function fixDuplicateResourceGroups() {
if (!session.isLoggedIn()) {
return;
}
let duplicateCount = 0;
const workspaces = await models.workspace.all();
for (const workspace of workspaces) {
const resources = await store.findResourcesByDocId(workspace._id);
// No duplicates found
if (resources.length <= 1) {
continue;
}
// Fix duplicates
const ids = resources.map(r => r.resourceGroupId);
const { deleteResourceGroupIds } = await syncFixDupes(ids);
for (const idToDelete of deleteResourceGroupIds) {
await store.removeResourceGroup(idToDelete);
}
duplicateCount++;
}
if (duplicateCount) {
logger.debug(`Fixed ${duplicateCount}/${workspaces.length} duplicate synced Workspaces`);
}
}
export async function writePendingChanges() {
// First make a copy and clear pending changes
const changes = Object.assign({}, _pendingDBChanges);
_pendingDBChanges = {};
const keys = Object.keys(changes);
if (keys.length === 0) {
// No changes, just return
return;
}
for (const key of Object.keys(changes)) {
const [event, doc, timestamp] = changes[key];
await _handleChangeAndPush(event, doc, timestamp);
}
}
export async function push(resourceGroupId = null) {
if (_disabledForSession) {
logger.debug('Legacy sync is disabled for current session');
return;
}
if (!session.isLoggedIn()) {
return;
}
let allDirtyResources = [];
if (resourceGroupId) {
allDirtyResources = await store.findActiveDirtyResourcesForResourceGroup(resourceGroupId);
} else {
allDirtyResources = await store.findActiveDirtyResources();
}
if (!allDirtyResources.length) {
// No changes to push
return;
}
const dirtyResources = [];
for (const r of allDirtyResources) {
// Check if resource type is blacklisted by user
const config = await store.getConfig(r.resourceGroupId);
if (r.type === models.clientCertificate.type && config.syncDisableClientCertificates) {
logger.debug(`Skipping pushing blacklisted client certificate ${r.id}`);
continue;
}
if (r.type === models.cookieJar.type && config.syncDisableCookieJars) {
logger.debug(`Skipping pushing blacklisted cookie jar ${r.id}`);
continue;
}
dirtyResources.push(r);
}
let responseBody;
try {
responseBody = await syncPush(dirtyResources);
} catch (e) {
logger.error('Failed to push changes', e);
return;
}
const { updated, created, removed, conflicts } = responseBody;
// Update all resource versions with the ones that were returned
for (const { id, version } of updated) {
const resource = await store.getResourceByDocId(id);
await store.updateResource(resource, { version, dirty: false });
}
if (updated.length) {
logger.debug(`Push updated ${updated.length} resources`);
}
// Update all resource versions with the ones that were returned
for (const { id, version } of created) {
const resource = await store.getResourceByDocId(id);
await store.updateResource(resource, { version, dirty: false });
}
if (created.length) {
logger.debug(`Push created ${created.length} resources`);
}
// Update all resource versions with the ones that were returned
for (const { id, version } of removed) {
const resource = await store.getResourceByDocId(id);
await store.updateResource(resource, { version, dirty: false });
}
if (removed.length) {
logger.debug(`Push removed ${removed.length} resources`);
}
// Resolve conflicts
await db.bufferChanges();
for (const serverResource of conflicts) {
const localResource = await store.getResourceByDocId(
serverResource.id,
serverResource.resourceGroupId,
);
// On conflict, choose last edited one
const serverIsNewer = serverResource.lastEdited > localResource.lastEdited;
const winner = serverIsNewer ? serverResource : localResource;
// Update local resource
// NOTE: using localResource as the base to make sure we have _id
await store.updateResource(localResource, winner, {
version: serverResource.version, // Act as the server resource no matter what
dirty: !serverIsNewer, // It's dirty if we chose the local doc
});
// Decrypt the docs from the resources. Don't fetch the local doc from the
// app database, because it might have been deleted.
const winnerName = serverIsNewer ? 'Server' : 'Local';
logger.debug(`Resolved conflict for ${serverResource.id} (${winnerName})`);
// If the server won, update ourselves. If we won, we already have the
// latest version, so do nothing.
if (serverIsNewer) {
const doc = await decryptDoc(winner.resourceGroupId, winner.encContent);
if (winner.removed) {
await db.remove(doc, true);
} else {
await db.update(doc, true);
}
}
}
db.flushChangesAsync();
}
export async function pull(resourceGroupId = null, createMissingResources = true) {
if (_disabledForSession) {
logger.debug('Legacy sync is disabled for current session');
return;
}
if (!session.isLoggedIn()) {
return;
}
// Try to fix duplicates first. Don't worry if this is called a lot since if there
// are no duplicates found it doesn't contact the network.
await fixDuplicateResourceGroups();
let allResources;
if (createMissingResources) {
allResources = await getOrCreateAllActiveResources(resourceGroupId);
} else {
allResources = await store.allActiveResources(resourceGroupId);
}
let blacklistedConfigs;
if (resourceGroupId) {
// When doing specific sync, blacklist all configs except the one we're trying to sync.
const allConfigs = await store.allConfigs();
blacklistedConfigs = allConfigs.filter(c => c.resourceGroupId !== resourceGroupId);
} else {
// When doing a full sync, blacklist the inactive configs
blacklistedConfigs = await store.findInactiveConfigs(resourceGroupId);
}
const resources = allResources.map(r => ({
id: r.id,
resourceGroupId: r.resourceGroupId,
version: r.version,
removed: r.removed,
}));
const blacklistedResourceGroupIds = blacklistedConfigs.map(c => c.resourceGroupId);
const body = {
resources,
blacklist: blacklistedResourceGroupIds,
};
if (resources.length) {
logger.debug(`Pulling with ${resources.length} resources`);
}
let responseBody;
try {
responseBody = await syncPull(body);
} catch (e) {
logger.error('Failed to sync changes', e, body);
return;
}
const { updatedResources, createdResources, idsToPush, idsToRemove } = responseBody;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Insert all the created docs to the DB //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
await db.bufferChanges();
for (const serverResource of createdResources) {
let doc;
try {
const { resourceGroupId, encContent } = serverResource;
doc = await decryptDoc(resourceGroupId, encContent);
} catch (e) {
logger.warn('Failed to decode created resource', e, serverResource);
return;
}
// Check if resource type is blacklisted by user
const config = await store.getConfig(serverResource.resourceGroupId);
if (
serverResource.type === models.clientCertificate.type &&
config.syncDisableClientCertificates
) {
logger.debug(`[sync] Skipping pulling blacklisted client certificate ${serverResource.id}`);
continue;
}
if (serverResource.type === models.cookieJar.type && config.syncDisableCookieJars) {
logger.debug(`[sync] Skipping pulling blacklisted cookie jar ${serverResource.id}`);
continue;
}
// Update local Resource
try {
await store.insertResource(serverResource, { dirty: false });
} catch (e) {
// This probably means we already have it. This should never happen, but
// might due to a rare race condition.
logger.error('Failed to insert resource', e, serverResource);
return;
}
// NOTE: If the above Resource insert succeeded, that means we have safely
// insert the document. However, we're using an upsert here instead because
// it's very possible that the client already had that document locally.
// This might happen, for example, if the user logs out and back in again.
const existingDoc = await db.get(doc.type, doc._id);
if (existingDoc) {
await db.update(doc, true);
} else {
// Mark as not seen if we created a new workspace from sync
if (doc.type === models.workspace.type) {
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(doc._id);
await models.workspaceMeta.update(workspaceMeta, { hasSeen: false });
}
await db.insert(doc, true);
}
}
if (createdResources.length) {
logger.debug(`Pull created ${createdResources.length} resources`);
}
db.flushChangesAsync();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Save all the updated docs to the DB //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
await db.bufferChanges();
for (const serverResource of updatedResources) {
try {
const { resourceGroupId, encContent } = serverResource;
const doc = await decryptDoc(resourceGroupId, encContent);
// Update app database
// Needs to be upsert because we could be "undeleting" something
await db.upsert(doc, true);
// Update local resource
const resource = await store.getResourceByDocId(
serverResource.id,
serverResource.resourceGroupId,
);
await store.updateResource(resource, serverResource, { dirty: false });
} catch (e) {
logger.warn('Failed to decode updated resource', e, serverResource);
}
}
db.flushChangesAsync();
if (updatedResources.length) {
logger.debug(`Pull updated ${updatedResources.length} resources`);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Remove all the docs that need removing //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
await db.bufferChanges();
for (const id of idsToRemove) {
const resource = await store.getResourceByDocId(id);
if (!resource) {
throw new Error(`Could not find Resource to remove for ${id}`);
}
const doc = await decryptDoc(resource.resourceGroupId, resource.encContent);
if (!doc) {
throw new Error(`Could not find doc to remove ${id}`);
}
// Mark resource as deleted
await store.updateResource(resource, { dirty: false, removed: true });
// Remove from DB
await db.remove(doc, true);
}
db.flushChangesAsync();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Push all the docs that need pushing //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
for (const id of idsToPush) {
const resource = await store.getResourceByDocId(id);
if (!resource) {
throw new Error(`Could not find Resource to push for id ${id}`);
}
// Mark all resources to push as dirty for the next push
await store.updateResource(resource, { dirty: true });
}
return updatedResources.length + createdResources.length;
}
export async function getOrCreateConfig(resourceGroupId) {
const config = await store.getConfig(resourceGroupId);
if (!config) {
return store.insertConfig({ resourceGroupId });
} else {
return config;
}
}
export async function ensureConfigExists(resourceGroupId, syncMode) {
const config = await store.getConfig(resourceGroupId);
if (!config) {
await store.insertConfig({ resourceGroupId, syncMode });
}
}
export async function createOrUpdateConfig(resourceGroupId, patch) {
const config = await store.getConfig(resourceGroupId);
const finalPatch = { resourceGroupId, ...patch };
if (config) {
return store.updateConfig(config, finalPatch);
} else {
return store.insertConfig(finalPatch);
}
}
export async function logout() {
await session.logout();
await resetLocalData();
}
export async function cancelTrial() {
await session.endTrial();
await session.logout();
await resetLocalData();
}
export async function resetLocalData() {
for (const c of await store.allConfigs()) {
await store.removeConfig(c);
}
for (const r of await store.allResources()) {
await store.removeResource(r);
}
}
export async function resetRemoteData() {
await syncResetData();
}
// ~~~~~~~ //
// HELPERS //
// ~~~~~~~ //
async function _handleChangeAndPush(event, doc, timestamp) {
// Update the resource content and set dirty
// TODO: Remove one of these steps since it does encryption twice
// in the case where the resource does not exist yet
const resource = await getOrCreateResourceForDoc(doc);
const updatedResource = await store.updateResource(resource, {
name: doc.name || 'n/a',
lastEdited: timestamp,
lastEditedBy: session.getAccountId(),
encContent: await encryptDoc(resource.resourceGroupId, doc),
removed: event === db.CHANGE_REMOVE,
dirty: true,
});
// Debounce pushing of dirty resources
logger.debug(`Queue ${event} ${updatedResource.id}`);
}
/**
* Fetch a ResourceGroup. If it has been fetched before, lookup from memory
*
* @param resourceGroupId
* @returns {*}
*/
const _fetchResourceGroupPromises = {};
const _resourceGroupCache = {};
export async function fetchResourceGroup(resourceGroupId, invalidateCache = false) {
if (invalidateCache) {
delete _resourceGroupCache[resourceGroupId];
delete _fetchResourceGroupPromises[resourceGroupId];
}
// PERF: If we're currently fetching, return stored promise
// TODO: Maybe move parallel fetch caching into the fetch helper
if (_fetchResourceGroupPromises[resourceGroupId]) {
return _fetchResourceGroupPromises[resourceGroupId];
}
const promise = new Promise(async (resolve, reject) => {
let resourceGroup = _resourceGroupCache[resourceGroupId];
if (!resourceGroup) {
try {
resourceGroup = await syncGetResourceGroup(resourceGroupId);
} catch (e) {
if (e.statusCode === 404) {
await store.removeResourceGroup(resourceGroupId);
logger.debug('ResourceGroup not found. Deleting...');
reject(new Error('ResourceGroup was not found'));
return;
} else {
logger.error(`Failed to get ResourceGroup ${resourceGroupId}: ${e}`);
reject(e);
return;
}
}
if (resourceGroup.isDisabled) {
await store.removeResourceGroup(resourceGroup.id);
logger.debug('ResourceGroup was disabled. Deleting...');
reject(new Error('ResourceGroup was disabled'));
return;
}
// Also make sure a config exists when we first fetch it.
// (This may not be needed but we'll do it just in case)
await ensureConfigExists(resourceGroupId);
}
// Bust cached promise because we're done with it.
_fetchResourceGroupPromises[resourceGroupId] = null;
// Cache the ResourceGroup for next time (they never change)
_resourceGroupCache[resourceGroupId] = resourceGroup;
// Return the ResourceGroup
resolve(resourceGroup);
});
// Cache the Promise in case we get asked for the same thing before done
_fetchResourceGroupPromises[resourceGroupId] = promise;
return promise;
}
/**
* Get a ResourceGroup's symmetric encryption key
*
* @param resourceGroupId
* @private
*/
async function _getResourceGroupSymmetricKey(resourceGroupId) {
let key = resourceGroupSymmetricKeysCache[resourceGroupId];
if (!key) {
const resourceGroup = await fetchResourceGroup(resourceGroupId);
const accountPrivateKey = await session.getPrivateKey();
const symmetricKeyStr = crypt.decryptRSAWithJWK(
accountPrivateKey,
resourceGroup.encSymmetricKey,
);
key = JSON.parse(symmetricKeyStr);
// Update cache
resourceGroupSymmetricKeysCache[resourceGroupId] = key;
}
return key;
}
export async function encryptDoc(resourceGroupId, doc) {
try {
const symmetricKey = await _getResourceGroupSymmetricKey(resourceGroupId);
// TODO: Turn on compression once enough users are on version >= 5.7.0
// const jsonStr = JSON.stringify(doc);
// const docStr = zlib.gzipSync(jsonStr);
// Don't use compression for now
const docStr = JSON.stringify(doc);
const message = crypt.encryptAES(symmetricKey, docStr);
return JSON.stringify(message);
} catch (e) {
logger.error(`Failed to encrypt for ${resourceGroupId}: ${e}`);
throw e;
}
}
export async function decryptDoc(resourceGroupId, messageJSON) {
let decrypted;
try {
const symmetricKey = await _getResourceGroupSymmetricKey(resourceGroupId);
const message = JSON.parse(messageJSON);
decrypted = crypt.decryptAES(symmetricKey, message);
} catch (e) {
logger.error(`Failed to decrypt from ${resourceGroupId}: ${e}`, messageJSON);
throw e;
}
try {
decrypted = zlib.gunzipSync(decrypted);
} catch (err) {
// It's not compressed (legacy), which is okay for now
}
try {
return JSON.parse(decrypted);
} catch (e) {
logger.error(`Failed to parse after decrypt from ${resourceGroupId}: ${e}`, decrypted);
throw e;
}
}
async function _getWorkspaceForDoc(doc) {
const ancestors = await db.withAncestors(doc);
return ancestors.find(d => d.type === models.workspace.type);
}
export async function createResourceGroup(parentId, name) {
// Generate symmetric key for ResourceGroup
const rgSymmetricJWK = await crypt.generateAES256Key();
const rgSymmetricJWKStr = JSON.stringify(rgSymmetricJWK);
// Encrypt the symmetric key with Account public key
const publicJWK = session.getPublicKey();
const encRGSymmetricJWK = crypt.encryptRSAWithJWK(publicJWK, rgSymmetricJWKStr);
// Create the new ResourceGroup
let resourceGroup;
try {
resourceGroup = await syncCreateResourceGroup(parentId, name, encRGSymmetricJWK);
} catch (e) {
logger.error(`Failed to create ResourceGroup: ${e}`);
throw e;
}
// Create a config for it
await ensureConfigExists(resourceGroup.id, store.SYNC_MODE_UNSET);
logger.debug(`Created ResourceGroup ${resourceGroup.id}`);
return resourceGroup;
}
export async function createResource(doc, resourceGroupId) {
return store.insertResource({
id: doc._id,
name: doc.name || 'n/a', // Set name to the doc name if it has one
resourceGroupId: resourceGroupId,
version: NO_VERSION,
createdBy: session.getAccountId(),
lastEdited: doc.modified,
lastEditedBy: session.getAccountId(),
removed: false,
type: doc.type,
encContent: await encryptDoc(resourceGroupId, doc),
dirty: true,
});
}
export async function createResourceForDoc(doc) {
// No resource yet, so create one
const workspace = await _getWorkspaceForDoc(doc);
if (!workspace) {
// Workspace was probably deleted before it's children could be synced.
// TODO: Handle this case better
throw new Error(`Could not find workspace for doc ${doc._id}`);
}
let workspaceResource = await store.getResourceByDocId(workspace._id);
if (!workspaceResource) {
const workspaceResourceGroup = await createResourceGroup(workspace._id, workspace.name);
workspaceResource = await createResource(workspace, workspaceResourceGroup.id);
}
if (workspace === doc) {
// If the current doc IS a Workspace, just return it
return workspaceResource;
} else {
return createResource(doc, workspaceResource.resourceGroupId);
}
}
export async function getOrCreateResourceForDoc(doc) {
const [resource, ...extras] = await store.findResourcesByDocId(doc._id);
// Sometimes there may be multiple resources created by accident for
// the same doc. Let's delete the extras here if there are any.
for (const resource of extras) {
await store.removeResource(resource);
}
if (resource) {
return resource;
} else {
return createResourceForDoc(doc);
}
}
export async function getOrCreateAllActiveResources(resourceGroupId = null) {
const startTime = Date.now();
const activeResourceMap = {};
let activeResources;
if (resourceGroupId) {
activeResources = await store.activeResourcesForResourceGroup(resourceGroupId);
} else {
activeResources = await store.allActiveResources();
}
for (const r of activeResources) {
activeResourceMap[r.id] = r;
}
// Make sure Workspace is first, because the loop below depends on it
const modelTypes = Object.keys(WHITE_LIST).sort((a, b) =>
a.type === models.workspace.type ? 1 : -1,
);
let created = 0;
for (const type of modelTypes) {
for (const doc of await db.all(type)) {
if (doc.isPrivate) {
// logger.debug(`Skip private doc ${doc._id}`);
continue;
}
const resource = await store.getResourceByDocId(doc._id);
if (!resource) {
try {
activeResourceMap[doc._id] = await createResourceForDoc(doc);
created++;
} catch (e) {
// logger.warn(`Failed to create resource for ${doc._id} ${e}`, {doc});
}
}
}
}
const resources = Object.keys(activeResourceMap).map(k => activeResourceMap[k]);
const time = (Date.now() - startTime) / 1000;
if (created > 0) {
logger.debug(`Created ${created}/${resources.length} Resources (${time.toFixed(2)}s)`);
}
return resources;
}

View File

@@ -1,37 +0,0 @@
export default class Logger {
constructor() {
this._logs = [];
}
debug(message, ...args) {
this._log('debug', message, ...args);
}
warn(message, ...args) {
this._log('warn', message, ...args);
}
error(message, ...args) {
this._log('error', message, ...args);
}
tail() {
return this._logs;
}
/** @private */
_log(type, message, ...args) {
let fn;
if (type === 'debug') {
fn = 'log';
} else if (type === 'warn') {
fn = 'warn';
} else {
fn = 'error';
}
console[fn](`[sync] ${message}`, ...args);
const date = new Date();
this._logs.push({ type, date, message });
}
}

View File

@@ -1,77 +0,0 @@
import * as fetch from '../account/fetch';
import * as session from '../account/session';
import * as crypt from '../account/crypt';
export async function syncCreateResourceGroup(parentResourceId, name, encSymmetricKey) {
return fetch.post(
'/api/resource_groups',
{
parentResourceId,
name,
encSymmetricKey,
},
session.getCurrentSessionId(),
);
}
export async function syncGetResourceGroup(id) {
return fetch.get(`/api/resource_groups/${id}`, session.getCurrentSessionId());
}
export async function syncPull(body) {
return fetch.post('/sync/pull', body, session.getCurrentSessionId(), true);
}
export async function syncPush(body) {
return fetch.post('/sync/push', body, session.getCurrentSessionId(), true);
}
export async function syncResetData() {
return fetch.post('/auth/reset', null, session.getCurrentSessionId());
}
export async function syncFixDupes(resourceGroupIds) {
return fetch.post('/sync/fix-dupes', { ids: resourceGroupIds }, session.getCurrentSessionId());
}
export async function unshareWithAllTeams(resourceGroupId) {
return fetch.put(
`/api/resource_groups/${resourceGroupId}/unshare`,
null,
session.getCurrentSessionId(),
);
}
export async function shareWithTeam(resourceGroupId, teamId) {
// Ask the server what we need to do to invite the member
const instructions = await fetch.post(
`/api/resource_groups/${resourceGroupId}/share-a`,
{
teamId,
},
session.getCurrentSessionId(),
);
const privateKeyJWK = session.getPrivateKey();
const resourceGroupSymmetricKey = crypt.decryptRSAWithJWK(
privateKeyJWK,
instructions.encSymmetricKey,
);
// Build the invite data request
const newKeys = {};
for (const accountId of Object.keys(instructions.keys)) {
const accountPublicKeyJWK = JSON.parse(instructions.keys[accountId]);
newKeys[accountId] = crypt.encryptRSAWithJWK(accountPublicKeyJWK, resourceGroupSymmetricKey);
}
// Actually share it with the team
await fetch.post(
`/api/resource_groups/${resourceGroupId}/share-b`,
{
teamId,
keys: newKeys,
},
session.getCurrentSessionId(),
);
}

View File

@@ -1,240 +0,0 @@
import NeDB from 'nedb';
import fsPath from 'path';
import crypto from 'crypto';
import * as util from '../common/misc';
import { DB_PERSIST_INTERVAL } from '../common/constants';
const TYPE_RESOURCE = 'Resource';
const TYPE_CONFIG = 'Config';
export const SYNC_MODE_OFF = 'paused';
export const SYNC_MODE_ON = 'active';
export const SYNC_MODE_NEVER = 'never';
export const SYNC_MODE_UNSET = 'unset';
let changeListeners = [];
export function onChange(callback) {
changeListeners.push(callback);
}
export function offChange(callback) {
changeListeners = changeListeners.filter(l => l !== callback);
}
let _changeTimeout = null;
function _notifyChange() {
clearTimeout(_changeTimeout);
_changeTimeout = setTimeout(() => {
for (const fn of changeListeners) {
fn();
}
}, 200);
}
export function allActiveResources(resourceGroupId = null) {
if (resourceGroupId) {
return findActiveResources({ resourceGroupId });
} else {
return findActiveResources({});
}
}
export function activeResourcesForResourceGroup(resourceGroupId) {
return findActiveResources({ resourceGroupId });
}
export function allResources() {
return findResources({});
}
export async function findResources(query = {}) {
return _execDB(TYPE_RESOURCE, 'find', query);
}
export async function findActiveResources(query) {
const configs = await findActiveConfigs();
const resourceGroupIds = configs.map(c => c.resourceGroupId);
return findResources(Object.assign({ resourceGroupId: { $in: resourceGroupIds } }, query));
}
export async function findActiveDirtyResources() {
return findActiveResources({ dirty: true });
}
export async function findActiveDirtyResourcesForResourceGroup(resourceGroupId) {
return findActiveResources({ dirty: true, resourceGroupId });
}
export async function findDirtyResourcesForResourceGroup(resourceGroupId) {
return findResources({ dirty: true, resourceGroupId });
}
export async function findResourcesForResourceGroup(resourceGroupId) {
return findResources({ resourceGroupId });
}
export async function getResourceByDocId(id, resourceGroupId = null) {
let query;
if (resourceGroupId) {
query = { id, resourceGroupId };
} else {
query = { id };
}
const rawDocs = await _execDB(TYPE_RESOURCE, 'find', query);
return rawDocs.length >= 1 ? rawDocs[0] : null;
}
/**
* This function is temporary and should only be called when cleaning
* up duplicate ResourceGroups
* @param id
* @returns {*}
*/
export function findResourcesByDocId(id) {
return _execDB(TYPE_RESOURCE, 'find', { id });
}
/**
* This function is temporary and should only be called when cleaning
* up duplicate ResourceGroups
* @param resourceGroupId
* @returns {*}
*/
export async function removeResourceGroup(resourceGroupId) {
await _execDB(TYPE_RESOURCE, 'remove', { resourceGroupId }, { multi: true });
await _execDB(TYPE_CONFIG, 'remove', { resourceGroupId }, { multi: true });
_notifyChange();
}
export async function insertResource(resource) {
const h = crypto.createHash('md5');
h.update(resource.resourceGroupId);
h.update(resource.id);
const newResource = Object.assign({}, resource, {
_id: `rs_${h.digest('hex')}`,
});
await _execDB(TYPE_RESOURCE, 'insert', newResource);
_notifyChange();
return newResource;
}
export async function updateResource(resource, ...patches) {
const newDoc = Object.assign({}, resource, ...patches);
await _execDB(TYPE_RESOURCE, 'update', { _id: resource._id }, newDoc, {
multi: true,
});
_notifyChange();
return newDoc;
}
export async function removeResource(resource) {
await _execDB(TYPE_RESOURCE, 'remove', { _id: resource._id }, { multi: true });
_notifyChange();
}
// ~~~~~~ //
// Config //
// ~~~~~~ //
export function findConfigs(query) {
return _execDB(TYPE_CONFIG, 'find', query);
}
export function allConfigs() {
return findConfigs({});
}
export function findInactiveConfigs(excludedResourceGroupId = null) {
if (excludedResourceGroupId) {
return findConfigs({
$not: { syncMode: SYNC_MODE_ON, excludedResourceGroupId },
});
} else {
return findConfigs({ $not: { syncMode: SYNC_MODE_ON } });
}
}
export function findActiveConfigs(resourceGroupId = null) {
if (resourceGroupId) {
return findConfigs({ syncMode: SYNC_MODE_ON, resourceGroupId });
} else {
return findConfigs({ syncMode: SYNC_MODE_ON });
}
}
export async function getConfig(resourceGroupId) {
const rawDocs = await _execDB(TYPE_CONFIG, 'find', { resourceGroupId });
return rawDocs.length >= 1 ? _initConfig(rawDocs[0]) : null;
}
export async function updateConfig(config, ...patches) {
const doc = _initConfig(Object.assign(config, ...patches));
await _execDB(TYPE_CONFIG, 'update', { _id: doc._id }, doc);
return doc;
}
export function removeConfig(config) {
return _execDB(TYPE_CONFIG, 'remove', { _id: config._id });
}
export async function insertConfig(config) {
const doc = _initConfig(config);
await _execDB(TYPE_CONFIG, 'insert', doc);
return doc;
}
function _initConfig(data) {
return Object.assign(
{
_id: util.generateId('scf'),
syncMode: SYNC_MODE_UNSET,
resourceGroupId: null,
},
data,
);
}
export function initDB(config, forceReset) {
if (!_database || forceReset) {
const basePath = util.getDataDirectory();
_database = {};
// NOTE: Do not EVER change this. EVER!
const resourcePath = fsPath.join(basePath, 'sync/Resource.db');
const configPath = fsPath.join(basePath, 'sync/Config.db');
// Fill in the defaults
_database.Resource = new NeDB(
Object.assign({ filename: resourcePath, autoload: true }, config),
);
_database.Config = new NeDB(Object.assign({ filename: configPath, autoload: true }, config));
for (const key of Object.keys(_database)) {
_database[key].persistence.setAutocompactionInterval(DB_PERSIST_INTERVAL);
}
// Done
console.log(`[sync] Initialize Sync DB at ${basePath}`);
}
}
// ~~~~~~~ //
// Helpers //
// ~~~~~~~ //
let _database = null;
function _getDB(type, config = {}) {
initDB(config);
return _database[type];
}
function _execDB(type, fnName, ...args) {
return new Promise((resolve, reject) => {
_getDB(type)[fnName](...args, (err, data) => {
err ? reject(err) : resolve(data);
});
});
}

View File

@@ -1,234 +0,0 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
import { showModal } from '../modals';
import * as syncStorage from '../../../sync-legacy/storage';
import * as sync from '../../../sync-legacy';
import WorkspaceShareSettingsModal from '../modals/workspace-share-settings-modal';
import SetupSyncModal from '../modals/setup-sync-modal';
import type { Workspace } from '../../../models/workspace';
import * as session from '../../../account/session';
import { clickLink } from '../../../common/misc';
type Props = {
workspace: Workspace,
// Optional
className?: string,
};
type State = {
loggedIn: boolean | null,
loading: boolean,
resourceGroupId: string | null,
syncMode: string | null,
syncPercent: number,
workspaceName: string,
};
@autobind
class SyncLegacyDropdown extends React.PureComponent<Props, State> {
_hasPrompted: boolean;
_isMounted: boolean;
constructor(props: Props) {
super(props);
this._hasPrompted = false;
this._isMounted = false;
this.state = {
loggedIn: null,
loading: false,
resourceGroupId: null,
syncMode: null,
syncPercent: 0,
workspaceName: '',
};
}
_handleShowShareSettings() {
showModal(WorkspaceShareSettingsModal, { workspace: this.props.workspace });
}
async _handleSyncResourceGroupId() {
const { resourceGroupId } = this.state;
// Set loading state
this.setState({ loading: true });
await sync.getOrCreateConfig(resourceGroupId);
await sync.pull(resourceGroupId);
await sync.push(resourceGroupId);
await this._reloadData();
// Unset loading state
this.setState({ loading: false });
}
async _reloadData() {
const loggedIn = session.isLoggedIn();
if (loggedIn !== this.state.loggedIn) {
this.setState({ loggedIn });
}
if (!loggedIn) {
return;
}
// Get or create any related sync data
const { workspace } = this.props;
const { resourceGroupId } = await sync.getOrCreateResourceForDoc(workspace);
const config = await sync.getOrCreateConfig(resourceGroupId);
// Analyze it
const dirty = await syncStorage.findDirtyResourcesForResourceGroup(resourceGroupId);
const all = await syncStorage.findResourcesForResourceGroup(resourceGroupId);
const numClean = all.length - dirty.length;
const syncPercent = all.length === 0 ? 100 : parseInt((numClean / all.length) * 1000) / 10;
if (this._isMounted) {
this.setState({
resourceGroupId,
syncPercent,
syncMode: config.syncMode,
workspaceName: workspace.name,
});
}
}
static _handleShowSyncBetaPrompt() {
clickLink('https://support.insomnia.rest/article/67-version-control');
}
async _handleShowSyncModePrompt() {
showModal(SetupSyncModal, {
onSelectSyncMode: async syncMode => {
await this._reloadData();
},
});
}
componentDidMount() {
this._isMounted = true;
syncStorage.onChange(this._reloadData);
this._reloadData();
}
componentWillUnmount() {
syncStorage.offChange(this._reloadData);
this._isMounted = false;
}
componentDidUpdate() {
const { resourceGroupId, syncMode } = this.state;
if (!resourceGroupId) {
return;
}
// Sync has not yet been configured for this workspace, so prompt the user to do so
const isModeUnset = !syncMode || syncMode === syncStorage.SYNC_MODE_UNSET;
if (isModeUnset && !this._hasPrompted) {
this._hasPrompted = true;
this._handleShowSyncModePrompt();
}
}
_getSyncDescription(syncMode: string | null, syncPercentage: number) {
let el = null;
if (syncMode === syncStorage.SYNC_MODE_NEVER) {
el = <span>Sync Disabled</span>;
} else if (syncPercentage === 100) {
el = <span>Sync Up To Date</span>;
} else if (syncMode === syncStorage.SYNC_MODE_OFF) {
el = (
<span>
<i className="fa fa-pause-circle-o" /> Sync Required
</span>
);
} else if (syncMode === syncStorage.SYNC_MODE_ON) {
el = <span>Sync Pending</span>;
} else if (!syncMode || syncMode === syncStorage.SYNC_MODE_UNSET) {
el = (
<span>
<i className="fa fa-exclamation-circle" /> Configure Sync
</span>
);
}
return el;
}
render() {
const { className } = this.props;
const { resourceGroupId, loading, loggedIn } = this.state;
// Don't show the sync menu unless we're logged in
if (!loggedIn) {
return null;
}
if (!resourceGroupId) {
return (
<div className={className}>
<button className="btn btn--compact wide" disabled>
Initializing Sync...
</button>
</div>
);
} else {
const { syncMode, syncPercent } = this.state;
return (
<div className={className}>
<Dropdown wide className="wide tall">
<DropdownButton className="btn btn--compact wide">
{this._getSyncDescription(syncMode, syncPercent)}
</DropdownButton>
<DropdownDivider>Workspace Synced {syncPercent}%</DropdownDivider>
<DropdownItem onClick={this._handleShowSyncModePrompt}>
<i className="fa fa-wrench" />
Change Sync Mode
</DropdownItem>
{/* SYNCED */}
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
<DropdownItem onClick={this._handleSyncResourceGroupId} stayOpenAfterClick>
{loading ? (
<i className="fa fa-refresh fa-spin" />
) : (
<i className="fa fa-cloud-upload" />
)}
Sync Now
</DropdownItem>
) : null}
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
<DropdownItem onClick={this._handleShowShareSettings}>
<i className="fa fa-users" />
Share Settings
</DropdownItem>
) : null}
{syncMode === syncStorage.SYNC_MODE_OFF && [
// NOTE: We can't use <React.Fragment> here because the nesting breaks
// the <Dropdown> component's child detection
<DropdownDivider key="divider" />,
<DropdownItem key="beta" onClick={SyncLegacyDropdown._handleShowSyncBetaPrompt}>
<i className="fa fa-star" />
Try New Sync Beta
</DropdownItem>,
]}
</Dropdown>
</div>
);
}
}
}
export default SyncLegacyDropdown;

View File

@@ -13,7 +13,6 @@ import { getAppName, getAppVersion } from '../../../common/constants';
import { showAlert, showError, showModal, showPrompt } from '../modals';
import Link from '../base/link';
import WorkspaceSettingsModal from '../modals/workspace-settings-modal';
import WorkspaceShareSettingsModal from '../modals/workspace-share-settings-modal';
import LoginModal from '../modals/login-modal';
import Tooltip from '../tooltip';
import KeydownBinder from '../keydown-binder';
@@ -26,7 +25,6 @@ import * as db from '../../../common/database';
import VCS from '../../../sync/vcs';
import HelpTooltip from '../help-tooltip';
import type { Project } from '../../../sync/types';
import * as sync from '../../../sync-legacy/index';
import PromptButton from '../base/prompt-button';
import * as session from '../../../account/session';
import type { WorkspaceAction } from '../../../plugins';
@@ -38,7 +36,6 @@ import type { Environment } from '../../../models/environment';
type Props = {
activeEnvironment: Environment | null,
activeWorkspace: Workspace,
enableSyncBeta: boolean,
handleSetActiveWorkspace: (id: string) => void,
hotKeyRegistry: HotKeyRegistry,
isLoading: boolean,
@@ -197,7 +194,7 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
}
static async _handleLogout() {
await sync.logout();
await session.logout();
}
static _handleShowExport() {
@@ -213,11 +210,7 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
}
_handleShowShareSettings() {
if (this.props.enableSyncBeta) {
showModal(SyncShareModal);
} else {
showModal(WorkspaceShareSettingsModal);
}
showModal(SyncShareModal);
}
_handleWorkspaceCreate() {
@@ -259,7 +252,6 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
isLoading,
hotKeyRegistry,
handleSetActiveWorkspace,
enableSyncBeta,
...other
} = this.props;

View File

@@ -5,7 +5,6 @@ import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
import * as sync from '../../../sync-legacy';
import * as session from '../../../account/session';
@autobind
@@ -42,10 +41,6 @@ class LoginModal extends PureComponent {
try {
await session.login(email, password);
// Clear all existing sync data that might be there and enable sync
await sync.resetLocalData();
await sync.doInitialSync();
this.hide();
} catch (e) {
this.setState({ error: e.message, loading: false });

View File

@@ -5,8 +5,7 @@ import Link from '../base/link';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import * as sync from '../../../sync-legacy/index';
import { getFirstName } from '../../../account/session';
import { getFirstName, endTrial, logout } from '../../../account/session';
let hidePaymentNotificationUntilNextLaunch = false;
@@ -14,7 +13,8 @@ let hidePaymentNotificationUntilNextLaunch = false;
class PaymentNotificationModal extends PureComponent {
async _handleCancel() {
try {
await sync.cancelTrial();
await endTrial();
await logout();
} catch (err) {
// That's okay
}

View File

@@ -1,181 +0,0 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
import * as sync from '../../../sync-legacy';
import {
SYNC_MODE_OFF,
SYNC_MODE_ON,
SYNC_MODE_NEVER,
SYNC_MODE_UNSET,
} from '../../../sync-legacy/storage';
import type { Workspace } from '../../../models/workspace';
import HelpTooltip from '../help-tooltip';
type Props = {
workspace: Workspace,
};
type State = {
syncMode: string,
selectedSyncMode: string,
syncDisableCookieJars: boolean,
syncDisableClientCertificates: boolean,
};
@autobind
class SetupSyncModal extends React.PureComponent<Props, State> {
modal: ?Modal;
_onSelectSyncMode: ?(selectedSyncMode: string) => void;
constructor(props: Props) {
super(props);
this.state = {
syncMode: SYNC_MODE_UNSET,
selectedSyncMode: SYNC_MODE_ON,
syncDisableCookieJars: false,
syncDisableClientCertificates: false,
};
}
_setModalRef(n: ?Modal) {
this.modal = n;
}
_handleToggleSyncCertificates(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ syncDisableClientCertificates: !e.currentTarget.checked });
}
_handleToggleSyncCookieJars(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ syncDisableCookieJars: !e.currentTarget.checked });
}
async _handleDone() {
const { workspace } = this.props;
const { selectedSyncMode, syncDisableClientCertificates, syncDisableCookieJars } = this.state;
const resource = await sync.getOrCreateResourceForDoc(workspace);
await sync.createOrUpdateConfig(resource.resourceGroupId, {
syncMode: selectedSyncMode,
syncDisableClientCertificates: !!syncDisableClientCertificates,
syncDisableCookieJars: !!syncDisableCookieJars,
});
this.hide();
this._onSelectSyncMode && this._onSelectSyncMode(selectedSyncMode);
}
_handleSyncModeChange(e: SyntheticEvent<HTMLSelectElement>) {
const selectedSyncMode = e.currentTarget.value;
this.setState({
selectedSyncMode,
});
}
async show(options: { onSelectSyncMode: (syncMode: string) => void }) {
const { workspace } = this.props;
const resource = await sync.getOrCreateResourceForDoc(workspace);
const config = await sync.getOrCreateConfig(resource.resourceGroupId);
const { syncMode, syncDisableCookieJars, syncDisableClientCertificates } = config;
// Set selected sync mode. If it's unset, default it to ON
const selectedSyncMode = syncMode !== SYNC_MODE_UNSET ? syncMode : SYNC_MODE_ON;
this.setState({
syncMode,
selectedSyncMode,
syncDisableCookieJars,
syncDisableClientCertificates,
});
this._onSelectSyncMode = options.onSelectSyncMode;
this.modal && this.modal.show();
}
hide() {
this.modal && this.modal.hide();
}
render() {
const { workspace } = this.props;
const {
syncMode,
selectedSyncMode,
syncDisableClientCertificates,
syncDisableCookieJars,
} = this.state;
return (
<Modal ref={this._setModalRef} noEscape>
<ModalHeader>Workspace Sync Setup</ModalHeader>
<ModalBody className="wide pad-left pad-right">
{syncMode === SYNC_MODE_UNSET ? (
<p className="notice info">
You have not yet configured sync for your <strong>{workspace.name}</strong> workspace.
</p>
) : null}
<br />
<div className="form-control form-control--outlined">
<label>
Sync mode
<HelpTooltip className="space-left">
Control how and when data for this workspace is synced with the server
</HelpTooltip>
<select onChange={this._handleSyncModeChange} value={selectedSyncMode}>
<option value={SYNC_MODE_ON}>Automatically sync changes</option>
<option value={SYNC_MODE_OFF}>Manually sync changes</option>
<option value={SYNC_MODE_NEVER}>Disable sync for this workspace</option>
</select>
</label>
</div>
<br />
<label className="bold">
Advanced Rules
<HelpTooltip className="space-left">
Customize sync for you or your team's needs by choosing which resources are synced for
this workspace
</HelpTooltip>
</label>
<div className="form-control form-control--thin">
<label>
Sync Cookie Jars
<input
type="checkbox"
checked={!syncDisableCookieJars}
onChange={this._handleToggleSyncCookieJars}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Sync SSL Client Certificates
<input
type="checkbox"
checked={!syncDisableClientCertificates}
onChange={this._handleToggleSyncCertificates}
/>
</label>
</div>
<br />
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm tall">
* This can be changed at any time
</div>
<button className="btn" onClick={this._handleDone}>
Continue
</button>
</ModalFooter>
</Modal>
);
}
}
export default SetupSyncModal;

View File

@@ -1,203 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import autobind from 'autobind-decorator';
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
import Link from '../base/link';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
import * as sync from '../../../sync-legacy/index';
import PromptButton from '../base/prompt-button';
import { shareWithTeam, unshareWithAllTeams } from '../../../sync-legacy/network';
import * as session from '../../../account/session';
@autobind
class WorkspaceShareSettingsModal extends PureComponent {
constructor(props) {
super(props);
this.state = {};
}
static _handleSubmit(e) {
e.preventDefault();
}
_handleClose() {
this.hide();
}
_setModalRef(n) {
this.modal = n;
}
async _handleUnshare() {
if (!session.isLoggedIn()) {
return;
}
const { resourceGroup } = this.state;
this._resetState({ loading: true });
try {
await unshareWithAllTeams(resourceGroup.id);
await this._load();
} catch (err) {
console.warn('Failed to unshare workspace', err);
this._resetState({ error: err.message, loading: false });
}
}
async _handleShareWithTeam(team) {
const { resourceGroup } = this.state;
this._resetState({ loading: true });
try {
await shareWithTeam(resourceGroup.id, team.id);
await this._load();
} catch (err) {
this._resetState({ error: err.message, loading: false });
}
}
async _load() {
if (!session.isLoggedIn()) {
this._resetState({});
return;
}
const { workspace } = this.props;
const resource = await sync.getOrCreateResourceForDoc(workspace);
const teams = await session.listTeams();
try {
const resourceGroup = await sync.fetchResourceGroup(resource.resourceGroupId, true);
this.setState({ teams, resourceGroup, loading: false, error: '' });
} catch (err) {
console.warn('Failed to fetch ResourceGroup', err);
this.setState({
error: 'No sync info found. Please try again.',
loading: false,
});
}
}
_resetState(patch = {}) {
this.setState(
Object.assign(
{
teams: [],
resourceGroup: null,
error: '',
loading: false,
},
patch,
),
);
}
async show() {
this._resetState();
this.modal.show();
// This takes a while, so do it after show()
await this._load();
}
hide() {
this.modal.hide();
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._resetState();
}
render() {
const { teams, resourceGroup, error, loading } = this.state;
const { workspace } = this.props;
return (
<form onSubmit={WorkspaceShareSettingsModal._handleSubmit}>
<Modal ref={this._setModalRef}>
<ModalHeader key="header">Share Workspace</ModalHeader>
<ModalBody key="body" className="pad text-center" noScroll>
<p>
Share <strong>{workspace.name}</strong> to automatically sync your API workspace with
your team members.
</p>
<div className="form-control pad">
{error ? <div className="danger">Oops: {error}</div> : null}
<Dropdown outline>
<DropdownDivider>Teams</DropdownDivider>
{!loading ? (
resourceGroup && resourceGroup.teamId ? (
<DropdownButton className="btn btn--clicky">
<i className="fa fa-users" /> Shared with{' '}
<strong>{resourceGroup.teamName}</strong> <i className="fa fa-caret-down" />
</DropdownButton>
) : (
<DropdownButton className="btn btn--clicky">
<i className="fa fa-lock" /> Private <i className="fa fa-caret-down" />
</DropdownButton>
)
) : (
<DropdownButton className="btn btn--clicky">
<i className="fa fa-spin fa-refresh" /> Loading...{' '}
<i className="fa fa-caret-down" />
</DropdownButton>
)}
{teams.map(team => (
<DropdownItem key={team.id} value={team} onClick={this._handleShareWithTeam}>
<i className="fa fa-users" /> Share with <strong>{team.name}</strong>
</DropdownItem>
))}
{teams.length === 0 && (
<DropdownItem disabled onClick={this._handleShareWithTeam}>
<i className="fa fa-warning" /> You have no teams
</DropdownItem>
)}
<DropdownDivider>Other</DropdownDivider>
<DropdownItem
addIcon
buttonClass={PromptButton}
confirmMessage="Really make private?"
onClick={this._handleUnshare}>
<i className="fa fa-lock" /> Private
</DropdownItem>
</Dropdown>
&nbsp;&nbsp;
{session.isLoggedIn() ? (
<Link
button
className="btn btn--super-compact inline-block"
href="https://insomnia.rest/app/teams/">
Manage Teams
</Link>
) : (
<Link
button
className="btn btn--super-compact inline-block"
href="https://insomnia.rest/teams/">
Manage Teams
</Link>
)}
</div>
</ModalBody>
<ModalFooter key="footer">
<button type="button" className="btn" onClick={this._handleClose}>
Done
</button>
</ModalFooter>
</Modal>
</form>
);
}
}
WorkspaceShareSettingsModal.propTypes = {
workspace: PropTypes.object.isRequired,
};
export default WorkspaceShareSettingsModal;

View File

@@ -123,7 +123,6 @@ class PageLayout extends React.PureComponent<Props, State> {
ref={handleSetSidebarRef}
activeEnvironment={activeEnvironment}
activeGitRepository={activeGitRepository}
enableSyncBeta={settings.enableSyncBeta}
environmentHighlightColorStyle={settings.environmentHighlightColorStyle}
handleInitializeEntities={handleInitializeEntities}
handleSetActiveEnvironment={handleSetActiveEnvironment}

View File

@@ -1,7 +1,6 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import * as sync from '../../../sync-legacy/index';
import Link from '../base/link';
import LoginModal from '../modals/login-modal';
import { hideAllModals, showModal } from '../modals/index';
@@ -81,7 +80,7 @@ class Account extends React.PureComponent<Props, State> {
}
async _handleLogout() {
await sync.logout();
await session.logout();
this.forceUpdate();
}

View File

@@ -19,7 +19,6 @@ import {
} from '../../../common/constants';
import type { Settings } from '../../../models/settings';
import { setFont } from '../../../plugins/misc';
import * as session from '../../../account/session';
import Tooltip from '../tooltip';
import CheckForUpdatesButton from '../check-for-updates-button';
import { initNewOAuthSession } from '../../../network/o-auth-2/misc';
@@ -519,13 +518,6 @@ class General extends React.PureComponent<Props, State> {
as request data, names, etc.
</p>
</div>
{session.isLoggedIn() && (
<React.Fragment>
<hr />
{this.renderBooleanSetting('Enable version control beta', 'enableSyncBeta', '', true)}
</React.Fragment>
)}
</div>
);
}

View File

@@ -7,14 +7,12 @@ import type { Environment } from '../../../models/environment';
import classnames from 'classnames';
import { COLLAPSE_SIDEBAR_REMS, SIDEBAR_SKINNY_REMS } from '../../../common/constants';
import SyncDropdown from '../dropdowns/sync-dropdown';
import SyncLegacyDropdown from '../dropdowns/sync-legacy-dropdown';
import type { StatusCandidate } from '../../../sync/types';
import { isLoggedIn } from '../../../account/session';
type Props = {|
activeEnvironment: Environment | null,
children: React.Node,
enableSyncBeta: boolean,
environmentHighlightColorStyle: string,
handleSetActiveEnvironment: Function,
handleSetActiveWorkspace: Function,
@@ -35,7 +33,6 @@ class Sidebar extends React.PureComponent<Props> {
const {
activeEnvironment,
children,
enableSyncBeta,
environmentHighlightColorStyle,
hidden,
syncItems,
@@ -61,7 +58,7 @@ class Sidebar extends React.PureComponent<Props> {
}}>
{children}
{enableSyncBeta && vcs && isLoggedIn() && (
{vcs && isLoggedIn() && (
<SyncDropdown
className="sidebar__footer"
workspace={workspace}
@@ -69,14 +66,6 @@ class Sidebar extends React.PureComponent<Props> {
syncItems={syncItems}
/>
)}
{!enableSyncBeta && (
<SyncLegacyDropdown
className="sidebar__footer"
key={workspace._id}
workspace={workspace}
/>
)}
</aside>
);
}

View File

@@ -142,7 +142,6 @@ class WrapperDebug extends React.PureComponent<Props> {
unseenWorkspaces={unseenWorkspaces}
hotKeyRegistry={settings.hotKeyRegistry}
handleSetActiveWorkspace={handleSetActiveWorkspace}
enableSyncBeta={settings.enableSyncBeta}
isLoading={isLoading}
vcs={vcs}
/>

View File

@@ -35,7 +35,6 @@ import RequestSwitcherModal from './modals/request-switcher-modal';
import SettingsModal from './modals/settings-modal';
import FilterHelpModal from './modals/filter-help-modal';
import RequestSettingsModal from './modals/request-settings-modal';
import SetupSyncModal from './modals/setup-sync-modal';
import SyncStagingModal from './modals/sync-staging-modal';
import GitRepositorySettingsModal from './modals/git-repository-settings-modal';
import GitStagingModal from './modals/git-staging-modal';
@@ -49,7 +48,6 @@ import SyncDeleteModal from './modals/sync-delete-modal';
import RequestRenderErrorModal from './modals/request-render-error-modal';
import WorkspaceEnvironmentsEditModal from './modals/workspace-environments-edit-modal';
import WorkspaceSettingsModal from './modals/workspace-settings-modal';
import WorkspaceShareSettingsModal from './modals/workspace-share-settings-modal';
import CodePromptModal from './modals/code-prompt-modal';
import * as db from '../../common/database';
import * as models from '../../models/index';
@@ -672,8 +670,6 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
isVariableUncovered={isVariableUncovered}
/>
<WorkspaceShareSettingsModal ref={registerModal} workspace={activeWorkspace} />
<GenerateCodeModal
ref={registerModal}
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
@@ -720,8 +716,6 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
isVariableUncovered={isVariableUncovered}
/>
<SetupSyncModal ref={registerModal} workspace={activeWorkspace} />
{gitVCS && (
<React.Fragment>
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} />

View File

@@ -5,16 +5,14 @@ import App from './containers/app';
import * as models from '../models';
import * as db from '../common/database';
import { init as initStore } from './redux/modules';
import * as legacySync from '../sync-legacy';
import { init as initPlugins } from '../plugins';
import './css/index.less';
import { getAppId, getAppLongName, isDevelopment } from '../common/constants';
import { getAppLongName, isDevelopment } from '../common/constants';
import { setFont, setTheme } from '../plugins/misc';
import { AppContainer } from 'react-hot-loader';
import { DragDropContext } from 'react-dnd';
import DNDBackend from './dnd-backend';
import { trackEvent } from '../common/analytics';
import { APP_ID_DESIGNER, APP_ID_INSOMNIA } from '../../config';
import * as styledComponents from 'styled-components';
import { initNewOAuthSession } from '../network/o-auth-2/misc';
import { initializeLogging } from '../common/log';
@@ -60,22 +58,6 @@ document.title = getAppLongName();
// render(App);
// });
}
const appId = getAppId();
// Legacy sync not part of Designer
if (appId === APP_ID_DESIGNER) {
legacySync.disableForSession();
} else if (appId === APP_ID_INSOMNIA) {
// Do things that can wait
const { enableSyncBeta } = await models.settings.getOrCreate();
if (enableSyncBeta) {
console.log('[app] Enabling sync beta');
legacySync.disableForSession();
} else {
process.nextTick(legacySync.init);
}
}
})();
// Export some useful things for dev