diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 501df2de25..57772a8281 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -82,13 +82,21 @@ exports.getGlobalDB = tenantId => { } /** - * Given a koa context this tries to find the correct tenant Global DB. + * Given a koa context this tries to extra what tenant is being accessed. */ -exports.getGlobalDBFromCtx = ctx => { +exports.getTenantIdFromCtx = ctx => { const user = ctx.user || {} const params = ctx.request.params || {} const query = ctx.request.query || {} - return exports.getGlobalDB(user.tenantId || params.tenantId || query.tenantId) + return user.tenantId || params.tenantId || query.tenantId +} + +/** + * Given a koa context this tries to find the correct tenant Global DB. + */ +exports.getGlobalDBFromCtx = ctx => { + const tenantId = exports.getTenantIdFromCtx(ctx) + return exports.getGlobalDB(tenantId) } /** diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index 3ba5b4d694..944d2ac527 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -3,6 +3,7 @@ const appController = require("../../../controllers/application") const CouchDB = require("../../../../db") const { AppStatus } = require("../../../../db/utils") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") +const { TENANT_ID } = require("../../../../tests/utilities/structures") function Request(appId, params) { this.appId = appId @@ -16,8 +17,8 @@ exports.getAllTableRows = async config => { return req.body } -exports.clearAllApps = async () => { - const req = { query: { status: AppStatus.DEV } } +exports.clearAllApps = async (tenantId = TENANT_ID) => { + const req = { query: { status: AppStatus.DEV }, user: { tenantId } } await appController.fetch(req) const apps = req.body if (!apps || apps.length <= 0) { diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index ceef2a3e91..da7ece2e89 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -10,20 +10,23 @@ const { basicScreen, basicLayout, basicWebhook, + TENANT_ID, } = require("./structures") const controllers = require("./controllers") const supertest = require("supertest") const { cleanup } = require("../../utilities/fileSystem") const { Cookies } = require("@budibase/auth").constants const { jwt } = require("@budibase/auth").auth +const auth = require("@budibase/auth") const { getGlobalDB } = require("@budibase/auth/db") const { createASession } = require("@budibase/auth/sessions") const { user: userCache } = require("@budibase/auth/cache") +const CouchDB = require("../../db") +auth.init(CouchDB) const GLOBAL_USER_ID = "us_uuid1" const EMAIL = "babs@babs.com" const PASSWORD = "babs_password" -const TENANT_ID = "default" class TestConfiguration { constructor(openServer = true) { @@ -52,7 +55,7 @@ class TestConfiguration { request.cookies = { set: () => {}, get: () => {} } request.config = { jwtSecret: env.JWT_SECRET } request.appId = this.appId - request.user = { appId: this.appId } + request.user = { appId: this.appId, tenantId: TENANT_ID } request.query = {} request.request = { body: config, @@ -78,7 +81,7 @@ class TestConfiguration { roles: roles || {}, tenantId: TENANT_ID, } - await createASession(id, "sessionid") + await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID }) if (builder) { user.builder = { global: true } } @@ -108,6 +111,7 @@ class TestConfiguration { const auth = { userId: GLOBAL_USER_ID, sessionId: "sessionid", + tenantId: TENANT_ID, } const app = { roleId: BUILTIN_ROLE_IDS.ADMIN, @@ -334,11 +338,12 @@ class TestConfiguration { if (!email || !password) { await this.createUser() } - await createASession(userId, "sessionid") + await createASession(userId, { sessionId: "sessionid", tenantId: TENANT_ID }) // have to fake this const auth = { userId, sessionId: "sessionid", + tenantId: TENANT_ID, } const app = { roleId: roleId, diff --git a/packages/server/src/tests/utilities/structures.js b/packages/server/src/tests/utilities/structures.js index 91996a7804..e4b2c7e1f0 100644 --- a/packages/server/src/tests/utilities/structures.js +++ b/packages/server/src/tests/utilities/structures.js @@ -4,6 +4,8 @@ const { createHomeScreen } = require("../../constants/screens") const { EMPTY_LAYOUT } = require("../../constants/layouts") const { cloneDeep } = require("lodash/fp") +exports.TENANT_ID = "default" + exports.basicTable = () => { return { name: "TestTable", diff --git a/packages/worker/scripts/jestSetup.js b/packages/worker/scripts/jestSetup.js index 07648f693f..374edfb946 100644 --- a/packages/worker/scripts/jestSetup.js +++ b/packages/worker/scripts/jestSetup.js @@ -3,3 +3,4 @@ const env = require("../src/environment") env._set("NODE_ENV", "jest") env._set("JWT_SECRET", "test-jwtsecret") env._set("LOG_LEVEL", "silent") +env._set("MULTI_TENANCY", true) diff --git a/packages/worker/src/api/controllers/global/auth.js b/packages/worker/src/api/controllers/global/auth.js index c3bbaf41a2..f576e697ea 100644 --- a/packages/worker/src/api/controllers/global/auth.js +++ b/packages/worker/src/api/controllers/global/auth.js @@ -74,6 +74,7 @@ exports.reset = async ctx => { }) } } catch (err) { + console.log(err) // don't throw any kind of error to the user, this might give away something } ctx.body = { @@ -88,7 +89,7 @@ exports.resetUpdate = async ctx => { const { resetCode, password } = ctx.request.body try { const userId = await checkResetPasswordCode(resetCode) - const db = new getGlobalDB(ctx.params.tenantId) + const db = getGlobalDB(ctx.params.tenantId) const user = await db.get(userId) user.password = await hash(password) await db.put(user) diff --git a/packages/worker/src/api/controllers/global/templates.js b/packages/worker/src/api/controllers/global/templates.js index e781cf5a89..cf9e988b6c 100644 --- a/packages/worker/src/api/controllers/global/templates.js +++ b/packages/worker/src/api/controllers/global/templates.js @@ -4,7 +4,7 @@ const { TemplateBindings, GLOBAL_OWNER, } = require("../../../constants") -const { getTemplates } = require("../../../constants/templates") +const { getTemplatesCtx } = require("../../../constants/templates") exports.save = async ctx => { const db = getGlobalDBFromCtx(ctx) @@ -45,23 +45,23 @@ exports.definitions = async ctx => { } exports.fetch = async ctx => { - ctx.body = await getTemplates(ctx) + ctx.body = await getTemplatesCtx(ctx) } exports.fetchByType = async ctx => { - ctx.body = await getTemplates(ctx, { + ctx.body = await getTemplatesCtx(ctx, { type: ctx.params.type, }) } exports.fetchByOwner = async ctx => { - ctx.body = await getTemplates(ctx, { + ctx.body = await getTemplatesCtx(ctx, { ownerId: ctx.params.ownerId, }) } exports.find = async ctx => { - ctx.body = await getTemplates(ctx, { + ctx.body = await getTemplatesCtx(ctx, { id: ctx.params.id, }) } diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index a9d761bce0..e0d1e6f107 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -265,12 +265,16 @@ exports.find = async ctx => { } exports.invite = async ctx => { - const { email, userInfo } = ctx.request.body + let { email, userInfo } = ctx.request.body const tenantId = ctx.user.tenantId const existing = await getGlobalUserByEmail(email, tenantId) if (existing) { ctx.throw(400, "Email address already in use.") } + if (!userInfo) { + userInfo = {} + } + userInfo.tenantId = tenantId await sendEmail(tenantId, email, EmailTemplatePurpose.INVITATION, { subject: "{{ company }} platform invitation", info: userInfo, @@ -293,6 +297,9 @@ exports.inviteAccept = async ctx => { email, ...info, } + ctx.user = { + tenantId: info.tenantId, + } // this will flesh out the body response await exports.save(ctx) } catch (err) { diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/tests/auth.spec.js index f55e7ac8bd..dacff30ce3 100644 --- a/packages/worker/src/api/routes/tests/auth.spec.js +++ b/packages/worker/src/api/routes/tests/auth.spec.js @@ -36,8 +36,8 @@ describe("/api/global/auth", () => { expect(sendMailMock).toHaveBeenCalled() const emailCall = sendMailMock.mock.calls[0][0] // after this URL there should be a code - const parts = emailCall.html.split(`http://localhost:10000/builder/auth/${TENANT_ID}/reset?code=`) - code = parts[1].split("\"")[0] + const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`) + code = parts[1].split("\"")[0].split("&")[0] expect(code).toBeDefined() }) diff --git a/packages/worker/src/api/routes/tests/email.spec.js b/packages/worker/src/api/routes/tests/email.spec.js index 027ad83fc3..c8c93658f7 100644 --- a/packages/worker/src/api/routes/tests/email.spec.js +++ b/packages/worker/src/api/routes/tests/email.spec.js @@ -1,5 +1,6 @@ const setup = require("./utilities") const { EmailTemplatePurpose } = require("../../../constants") +const { TENANT_ID } = require("./utilities/structures") // mock the email system const sendMailMock = jest.fn() @@ -29,6 +30,7 @@ describe("/api/global/email", () => { .send({ email: "test@test.com", purpose: EmailTemplatePurpose.INVITATION, + tenantId: TENANT_ID, }) .set(config.defaultHeaders()) .expect("Content-Type", /json/) diff --git a/packages/worker/src/api/routes/tests/users.spec.js b/packages/worker/src/api/routes/tests/users.spec.js index b753641803..f03f9e60be 100644 --- a/packages/worker/src/api/routes/tests/users.spec.js +++ b/packages/worker/src/api/routes/tests/users.spec.js @@ -1,4 +1,5 @@ const setup = require("./utilities") +const { TENANT_ID } = require("./utilities/structures") jest.mock("nodemailer") const sendMailMock = setup.emailMock() @@ -31,7 +32,7 @@ describe("/api/global/users", () => { const emailCall = sendMailMock.mock.calls[0][0] // after this URL there should be a code const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=") - code = parts[1].split("\"")[0] + code = parts[1].split("\"")[0].split("&")[0] expect(code).toBeDefined() }) diff --git a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js index 224b61cf0d..fe1bb68641 100644 --- a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js @@ -7,8 +7,10 @@ const { Configs, LOGO_URL } = require("../../../../constants") const { getGlobalUserByEmail } = require("@budibase/auth").utils const { createASession } = require("@budibase/auth/sessions") const { newid } = require("../../../../../../auth/src/hashing") - -const TENANT_ID = "default" +const { TENANT_ID } = require("./structures") +const auth = require("@budibase/auth") +const CouchDB = require("../../../../db") +auth.init(CouchDB) class TestConfiguration { constructor(openServer = true) { @@ -30,7 +32,7 @@ class TestConfiguration { request.cookies = { set: () => {}, get: () => {} } request.config = { jwtSecret: env.JWT_SECRET } request.appId = this.appId - request.user = { appId: this.appId } + request.user = { appId: this.appId, tenantId: TENANT_ID } request.query = {} request.request = { body: config, @@ -60,7 +62,7 @@ class TestConfiguration { null, controllers.users.save ) - await createASession("us_uuid1", "sessionid") + await createASession("us_uuid1", { sessionId: "sessionid", tenantId: TENANT_ID }) } } @@ -233,6 +235,7 @@ class TestConfiguration { { email: "testuser@test.com", password: "test@test.com", + tenantId: TENANT_ID, }, null, controllers.users.adminUser diff --git a/packages/worker/src/api/routes/tests/utilities/structures.js b/packages/worker/src/api/routes/tests/utilities/structures.js new file mode 100644 index 0000000000..16701ac3d7 --- /dev/null +++ b/packages/worker/src/api/routes/tests/utilities/structures.js @@ -0,0 +1 @@ +exports.TENANT_ID = "default" diff --git a/packages/worker/src/constants/templates/index.js b/packages/worker/src/constants/templates/index.js index 026ebf6b91..f9a7257ebd 100644 --- a/packages/worker/src/constants/templates/index.js +++ b/packages/worker/src/constants/templates/index.js @@ -6,7 +6,7 @@ const { GLOBAL_OWNER, } = require("../index") const { join } = require("path") -const { getTemplateParams, getGlobalDBFromCtx } = require("@budibase/auth/db") +const { getTemplateParams, getTenantIdFromCtx, getGlobalDB } = require("@budibase/auth/db") exports.EmailTemplates = { [EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile( @@ -48,8 +48,13 @@ exports.addBaseTemplates = (templates, type = null) => { return templates } -exports.getTemplates = async (ctx, { ownerId, type, id } = {}) => { - const db = getGlobalDBFromCtx(ctx) +exports.getTemplatesCtx = async (ctx, opts = {}) => { + const tenantId = getTenantIdFromCtx(ctx) + return exports.getTemplates(tenantId, opts) +} + +exports.getTemplates = async (tenantId, { ownerId, type, id} = {}) => { + const db = getGlobalDB(tenantId) const response = await db.allDocs( getTemplateParams(ownerId || GLOBAL_OWNER, id, { include_docs: true, @@ -66,7 +71,10 @@ exports.getTemplates = async (ctx, { ownerId, type, id } = {}) => { return exports.addBaseTemplates(templates, type) } -exports.getTemplateByPurpose = async (ctx, type, purpose) => { - const templates = await exports.getTemplates(ctx, { type }) +exports.getTemplateByPurpose = async ({ tenantId, ctx }, type, purpose) => { + if (!tenantId && ctx) { + tenantId = getTenantIdFromCtx(ctx) + } + const templates = await exports.getTemplates(tenantId, { type }) return templates.find(template => template.purpose === purpose) } diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index 38fafd1014..e3e558369c 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -60,6 +60,7 @@ async function getLinkCode(purpose, email, user, info = null) { /** * Builds an email using handlebars and the templates found in the system (default or otherwise). + * @param {string} tenantId the ID of the tenant which is sending the email. * @param {string} purpose the purpose of the email being built, e.g. invitation, password reset. * @param {string} email the address which it is being sent to for contextual purposes. * @param {object} context the context which is being used for building the email (hbs context). @@ -67,14 +68,14 @@ async function getLinkCode(purpose, email, user, info = null) { * @param {string|null} contents if using a custom template can supply contents for context. * @return {Promise} returns the built email HTML if all provided parameters were valid. */ -async function buildEmail(purpose, email, context, { user, contents } = {}) { +async function buildEmail(tenantId, purpose, email, context, { user, contents } = {}) { // this isn't a full email if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { throw `Unable to build an email of type ${purpose}` } let [base, body] = await Promise.all([ - getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), - getTemplateByPurpose(TYPE, purpose), + getTemplateByPurpose({ tenantId }, TYPE, EmailTemplatePurpose.BASE), + getTemplateByPurpose({ tenantId }, TYPE, purpose), ]) if (!base || !body) { throw "Unable to build email, missing base components" @@ -147,7 +148,7 @@ exports.sendEmail = async ( purpose, { workspaceId, user, from, contents, subject, info } = {} ) => { - const db = new getGlobalDB(tenantId) + const db = getGlobalDB(tenantId) let config = (await getSmtpConfiguration(db, workspaceId)) || {} if (Object.keys(config).length === 0 && !TEST_MODE) { throw "Unable to find SMTP configuration." @@ -159,7 +160,7 @@ exports.sendEmail = async ( const message = { from: from || config.from, to: email, - html: await buildEmail(purpose, email, context, { user, contents }), + html: await buildEmail(tenantId, purpose, email, context, { user, contents }), } if (subject || config.subject) { message.subject = await processString(subject || config.subject, context) diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index dfd139fb84..40fc4e227e 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -11,8 +11,16 @@ const env = require("../environment") const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}` const BASE_COMPANY = "Budibase" +function addTenantToUrl(url, tenantId) { + if (env.MULTI_TENANCY) { + const char = url.indexOf("?") === -1 ? "?" : "&" + url += `${char}tenantId=${tenantId}` + } + return url +} + exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { - const db = new getGlobalDB(tenantId) + const db = getGlobalDB(tenantId) // TODO: use more granular settings in the future if required let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} if (!settings || !settings.platformUrl) { @@ -26,7 +34,7 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { [InternalTemplateBindings.COMPANY]: settings.company || BASE_COMPANY, [InternalTemplateBindings.DOCS_URL]: settings.docsUrl || "https://docs.budibase.com/", - [InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl(`${URL}/login`), + [InternalTemplateBindings.LOGIN_URL]: checkSlashesInUrl(addTenantToUrl(`${URL}/login`, tenantId)), [InternalTemplateBindings.CURRENT_DATE]: new Date().toISOString(), [InternalTemplateBindings.CURRENT_YEAR]: new Date().getFullYear(), } @@ -35,13 +43,13 @@ exports.getSettingsTemplateContext = async (tenantId, purpose, code = null) => { case EmailTemplatePurpose.PASSWORD_RECOVERY: context[InternalTemplateBindings.RESET_CODE] = code context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl( - `${URL}/builder/auth/reset?code=${code}` + addTenantToUrl(`${URL}/builder/auth/reset?code=${code}`, tenantId) ) break case EmailTemplatePurpose.INVITATION: context[InternalTemplateBindings.INVITE_CODE] = code context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl( - `${URL}/builder/invite?code=${code}` + addTenantToUrl(`${URL}/builder/invite?code=${code}&tenantId=${tenantId}`, tenantId) ) break }