From 71016f5a1df1a7c5259d28292cbcc7dda479a02a Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Wed, 10 Jun 2020 21:39:30 +0100 Subject: [PATCH] application supports multiple concurrent client DB --- .../server/src/api/controllers/application.js | 45 ++++++++++++++++--- packages/server/src/api/controllers/auth.js | 9 +++- packages/server/src/api/controllers/client.js | 21 +++++++-- .../server/src/api/controllers/component.js | 4 +- .../server/src/api/controllers/instance.js | 5 ++- packages/server/src/api/controllers/user.js | 5 ++- .../src/api/routes/tests/application.spec.js | 33 ++++++++++++++ .../src/api/routes/tests/couchTestUtils.js | 3 +- 8 files changed, 108 insertions(+), 17 deletions(-) diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 9bd015c86a..0eb7818032 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -11,7 +11,7 @@ const { exec } = require("child_process") const sqrl = require("squirrelly") exports.fetch = async function(ctx) { - const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) + const db = new CouchDB(ClientDb.name(getClientId(ctx))) const body = await db.query("client/by_type", { include_docs: true, key: ["app"], @@ -21,16 +21,30 @@ exports.fetch = async function(ctx) { } exports.fetchAppPackage = async function(ctx) { - const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) + const clientId = await lookupClientId(ctx.params.applicationId) + const db = new CouchDB(ClientDb.name(clientId)) const application = await db.get(ctx.params.applicationId) ctx.body = await getPackageForBuilder(ctx.config, application) } exports.create = async function(ctx) { - const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) + const clientId = + (ctx.request.body && ctx.request.body.clientId) || env.CLIENT_ID + + if (!clientId) { + ctx.throw(400, "ClientId not suplied") + } + const appId = newid() + // insert an appId -> clientId lookup + const masterDb = new CouchDB("master") + await masterDb.put({ + _id: appId, + clientId, + }) + const db = new CouchDB(ClientDb.name(clientId)) const newApplication = { - _id: newid(), + _id: appId, type: "app", instances: [], userInstanceMap: {}, @@ -47,11 +61,10 @@ exports.create = async function(ctx) { const createInstCtx = { params: { - clientId: env.CLIENT_ID, applicationId: newApplication._id, }, request: { - body: { name: `dev-${env.CLIENT_ID}` }, + body: { name: `dev-${clientId}` }, }, } await instanceController.create(createInstCtx) @@ -61,6 +74,7 @@ exports.create = async function(ctx) { await runNpmInstall(newAppFolder) } + ctx.status = 200 ctx.body = newApplication ctx.message = `Application ${ctx.request.body.name} created successfully` } @@ -79,7 +93,6 @@ const createEmptyAppPackage = async (ctx, app) => { if (await exists(newAppFolder)) { ctx.throw(400, "App folder already exists for this application") - return } await copy(templateFolder, newAppFolder) @@ -99,6 +112,24 @@ const createEmptyAppPackage = async (ctx, app) => { return newAppFolder } +const lookupClientId = async appId => { + const masterDb = new CouchDB("master") + const { clientId } = await masterDb.get(appId) + return clientId +} + +const getClientId = ctx => { + const clientId = + (ctx.request.body && ctx.request.body.clientId) || + (ctx.query && ctx.query.clientId) || + env.CLIENT_ID + + if (!clientId) { + ctx.throw(400, "ClientId not suplied") + } + return clientId +} + const updateJsonFile = async (filePath, app) => { const json = await readFile(filePath, "utf8") const newJson = sqrl.Render(json, app) diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index de88c29643..f083d57ba6 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -2,7 +2,6 @@ const jwt = require("jsonwebtoken") const CouchDB = require("../../db") const ClientDb = require("../../db/clientDb") const bcrypt = require("../../utilities/bcrypt") -const env = require("../../environment") exports.authenticate = async ctx => { const { username, password } = ctx.request.body @@ -10,8 +9,14 @@ exports.authenticate = async ctx => { if (!username) ctx.throw(400, "Username Required.") if (!password) ctx.throw(400, "Password Required") + const masterDb = new CouchDB("master") + const { clientId } = await masterDb.get(ctx.params.appId) + + if (!clientId) { + ctx.throw(400, "ClientId not suplied") + } // find the instance that the user is associated with - const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) + const db = new CouchDB(ClientDb.name(clientId)) const appId = ctx.params.appId const app = await db.get(appId) const instanceId = app.userInstanceMap[username] diff --git a/packages/server/src/api/controllers/client.js b/packages/server/src/api/controllers/client.js index 433bf58c3b..400ccde358 100644 --- a/packages/server/src/api/controllers/client.js +++ b/packages/server/src/api/controllers/client.js @@ -6,13 +6,26 @@ exports.getClientId = async function(ctx) { } exports.create = async function(ctx) { - await create(env.CLIENT_ID) + const clientId = getClientId(ctx) + await create(clientId) ctx.status = 200 - ctx.message = `Client Database ${env.CLIENT_ID} successfully provisioned.` + ctx.message = `Client Database ${clientId} successfully provisioned.` } exports.destroy = async function(ctx) { - await destroy(env.CLIENT_ID) + const clientId = getClientId(ctx) + await destroy(clientId) ctx.status = 200 - ctx.message = `Client Database ${env.CLIENT_ID} successfully deleted.` + ctx.message = `Client Database ${clientId} successfully deleted.` +} + +const getClientId = ctx => { + const clientId = + (ctx.query && ctx.query.clientId) || + (ctx.body && ctx.body.clientId) || + env.CLIENT_ID + if (!clientId) { + ctx.throw(400, "ClientId not suplied") + } + return clientId } diff --git a/packages/server/src/api/controllers/component.js b/packages/server/src/api/controllers/component.js index 0b07826555..eedd3a8ceb 100644 --- a/packages/server/src/api/controllers/component.js +++ b/packages/server/src/api/controllers/component.js @@ -8,7 +8,9 @@ const { const env = require("../../environment") exports.fetchAppComponentDefinitions = async function(ctx) { - const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) + const masterDb = new CouchDB("master") + const { clientId } = await masterDb.get(ctx.params.appId) + const db = new CouchDB(ClientDb.name(clientId)) const app = await db.get(ctx.params.appId) const componentDefinitions = app.componentLibraries.reduce( diff --git a/packages/server/src/api/controllers/instance.js b/packages/server/src/api/controllers/instance.js index fe40cf9fb4..d8f6a07888 100644 --- a/packages/server/src/api/controllers/instance.js +++ b/packages/server/src/api/controllers/instance.js @@ -8,7 +8,10 @@ exports.create = async function(ctx) { const appShortId = ctx.params.applicationId.substring(0, 7) const instanceId = `inst_${appShortId}_${newid()}` const { applicationId } = ctx.params - const clientId = env.CLIENT_ID + + const masterDb = new CouchDB("master") + const { clientId } = await masterDb.get(applicationId) + const db = new CouchDB(instanceId) await db.put({ _id: "_design/database", diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 5810872989..d529619843 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -42,8 +42,11 @@ exports.create = async function(ctx) { const response = await database.post(user) + const masterDb = new CouchDB("master") + const { clientId } = await masterDb.get(appId) + // the clientDB needs to store a map of users against the app - const db = new CouchDB(clientDb.name(env.CLIENT_ID)) + const db = new CouchDB(clientDb.name(clientId)) const app = await db.get(appId) app.userInstanceMap = { diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 4c193613fd..7f64419778 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -5,6 +5,7 @@ const { destroyClientDatabase, builderEndpointShouldBlockNormalUsers, supertest, + TEST_CLIENT_ID, defaultHeaders, } = require("./couchTestUtils") @@ -51,6 +52,7 @@ describe("/applications", () => { body: { name: "My App" } }) }) + }) describe("fetch", () => { @@ -68,6 +70,37 @@ describe("/applications", () => { expect(res.body.length).toBe(2) }) + it("lists only applications in requested client databse", async () => { + await createApplication(request, "app1") + await createClientDatabase("new_client") + + const blah = await request + .post("/api/applications") + .send({ name: "app2", clientId: "new_client"}) + .set(defaultHeaders) + .expect('Content-Type', /json/) + //.expect(200) + + const client1Res = await request + .get(`/api/applications?clientId=${TEST_CLIENT_ID}`) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + expect(client1Res.body.length).toBe(1) + expect(client1Res.body[0].name).toBe("app1") + + const client2Res = await request + .get(`/api/applications?clientId=new_client`) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + expect(client2Res.body.length).toBe(1) + expect(client2Res.body[0].name).toBe("app2") + + }) + it("should apply authorization to endpoint", async () => { const otherApplication = await createApplication(request) const instance = await createInstance(request, otherApplication._id) diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index f70fc0ced1..6029e080cc 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -9,6 +9,7 @@ const { const TEST_CLIENT_ID = "test-client-id" +exports.TEST_CLIENT_ID = TEST_CLIENT_ID exports.supertest = async () => { let request let port = 4002 @@ -59,7 +60,7 @@ exports.createView = async (request, instanceId, view) => { return res.body } -exports.createClientDatabase = async () => await create(TEST_CLIENT_ID) +exports.createClientDatabase = async id => await create(id || TEST_CLIENT_ID) exports.createApplication = async (request, name = "test_application") => { const res = await request