1
0
Fork 0
mirror of synced 2024-07-02 21:10:43 +12:00

Further work, tenancy now working but some more work to be done.

This commit is contained in:
mike12345567 2021-07-16 18:04:49 +01:00
parent b7995dd61d
commit f3156fca06
20 changed files with 192 additions and 48 deletions

View file

@ -21,3 +21,5 @@ exports.Configs = {
SMTP: "smtp", SMTP: "smtp",
GOOGLE: "google", GOOGLE: "google",
} }
exports.DEFAULT_TENANT_ID = "default"

View file

@ -1,10 +1,10 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication") const Replication = require("./Replication")
const { getDB } = require("./index") const { getDB, getCouch } = require("./index")
const { DEFAULT_TENANT_ID } = require("../constants")
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_" const SEPARATOR = "_"
const DEFAULT_TENANT = "default"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
@ -75,7 +75,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
exports.getGlobalDB = tenantId => { exports.getGlobalDB = tenantId => {
// fallback for system pre multi-tenancy // fallback for system pre multi-tenancy
let dbName = exports.StaticDatabases.GLOBAL.name let dbName = exports.StaticDatabases.GLOBAL.name
if (tenantId && tenantId !== DEFAULT_TENANT) { if (tenantId && tenantId !== DEFAULT_TENANT_ID) {
dbName = `${tenantId}${SEPARATOR}${dbName}` dbName = `${tenantId}${SEPARATOR}${dbName}`
} }
return getDB(dbName) return getDB(dbName)
@ -192,7 +192,11 @@ exports.getDeployedAppID = appId => {
* different users/companies apps as there is no security around it - all apps are returned. * different users/companies apps as there is no security around it - all apps are returned.
* @return {Promise<object[]>} returns the app information document stored in each app database. * @return {Promise<object[]>} returns the app information document stored in each app database.
*/ */
exports.getAllApps = async ({ CouchDB, dev, all } = {}) => { exports.getAllApps = async ({ tenantId, dev, all } = {}) => {
if (!tenantId) {
tenantId = DEFAULT_TENANT_ID
}
const CouchDB = getCouch()
let allDbs = await CouchDB.allDbs() let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => const appDbNames = allDbs.filter(dbName =>
dbName.startsWith(exports.APP_PREFIX) dbName.startsWith(exports.APP_PREFIX)
@ -206,10 +210,15 @@ exports.getAllApps = async ({ CouchDB, dev, all } = {}) => {
} else { } else {
const response = await Promise.allSettled(appPromises) const response = await Promise.allSettled(appPromises)
const apps = response const apps = response
.filter(result => result.status === "fulfilled") .filter(result => result.status === "fulfilled" )
.map(({ value }) => value) .map(({ value }) => value)
.filter(app => {
const appTenant = !app.tenantId ? DEFAULT_TENANT_ID : app.tenantId
return tenantId === appTenant
})
if (!all) { if (!all) {
return apps.filter(app => { return apps.filter(app => {
if (dev) { if (dev) {
return isDevApp(app) return isDevApp(app)
} }

View file

@ -1,5 +1,5 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { UserStatus } = require("../../constants") const { UserStatus, DEFAULT_TENANT_ID } = require("../../constants")
const { compare } = require("../../hashing") const { compare } = require("../../hashing")
const env = require("../../environment") const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils") const { getGlobalUserByEmail } = require("../../utils")
@ -24,10 +24,9 @@ exports.authenticate = async function (ctx, email, password, done) {
if (!email) return done(null, false, "Email Required.") if (!email) return done(null, false, "Email Required.")
if (!password) return done(null, false, "Password Required.") if (!password) return done(null, false, "Password Required.")
const params = ctx.params || {} const params = ctx.params || {}
const query = ctx.query || {}
// use the request to find the tenantId // use the request to find the tenantId
const tenantId = params.tenantId || query.tenantId let tenantId = params.tenantId || DEFAULT_TENANT_ID
const dbUser = await getGlobalUserByEmail(email, tenantId) const dbUser = await getGlobalUserByEmail(email, tenantId)
if (dbUser == null) { if (dbUser == null) {
return done(null, false, { message: "User not found" }) return done(null, false, { message: "User not found" })
@ -41,7 +40,6 @@ exports.authenticate = async function (ctx, email, password, done) {
// authenticate // authenticate
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const sessionId = newid() const sessionId = newid()
const tenantId = dbUser.tenantId
await createASession(dbUser._id, { sessionId, tenantId }) await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(

View file

@ -44,6 +44,7 @@
</Body> </Body>
</Layout> </Layout>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Input label="Organisation" bind:value={adminUser.tenantId} />
<Input label="Email" bind:value={adminUser.email} /> <Input label="Email" bind:value={adminUser.email} />
<PasswordRepeatInput bind:password={adminUser.password} bind:error /> <PasswordRepeatInput bind:password={adminUser.password} bind:error />
</Layout> </Layout>

View file

@ -15,6 +15,7 @@
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte" import { onMount } from "svelte"
let tenantId = ""
let username = "" let username = ""
let password = "" let password = ""
@ -25,6 +26,7 @@
await auth.login({ await auth.login({
username, username,
password, password,
tenantId,
}) })
notifications.success("Logged in successfully") notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) { if ($auth?.user?.forceResetPassword) {
@ -64,6 +66,7 @@
<Divider noGrid /> <Divider noGrid />
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body> <Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Organisation" bind:value={tenantId} />
<Input label="Email" bind:value={username} /> <Input label="Email" bind:value={username} />
<Input <Input
label="Password" label="Password"

View file

@ -0,0 +1,95 @@
<script>
import {
ActionButton,
Body,
Button,
Divider,
Heading,
Input,
Layout,
notifications,
} from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { auth, organisation } from "stores/portal"
import GoogleButton from "./_components/GoogleButton.svelte"
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
let tenantId = ""
let username = ""
let password = ""
$: company = $organisation.company || "Budibase"
async function login() {
try {
await auth.login({
username,
password,
tenantId,
})
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) {
$goto("./reset")
} else {
if ($params["?returnUrl"]) {
window.location = decodeURIComponent($params["?returnUrl"])
} else {
notifications.success("Logged in successfully")
$goto("../portal")
}
}
} catch (err) {
console.error(err)
notifications.error("Invalid credentials")
}
}
function handleKeydown(evt) {
if (evt.key === "Enter") login()
}
onMount(async () => {
await organisation.init()
})
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img alt="logo" src={$organisation.logoUrl || Logo} />
<Heading>Sign in to {company}</Heading>
</Layout>
<GoogleButton />
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Organisation" bind:value={tenantId} />
</Layout>
<Layout gap="XS" noPadding>
<Button cta on:click={login}>Sign in to {company}</Button>
</Layout>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View file

@ -1,12 +1,14 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
import { auth } from "stores/portal"
export function createAdminStore() { export function createAdminStore() {
const { subscribe, set } = writable({}) const { subscribe, set } = writable({})
async function init() { async function init() {
try { try {
const response = await api.get("/api/admin/configs/checklist") const tenantId = get(auth).tenantId
const response = await api.get(`/api/admin/configs/checklist?tenantId=${tenantId}`)
const json = await response.json() const json = await response.json()
const onboardingSteps = Object.keys(json) const onboardingSteps = Object.keys(json)

View file

@ -21,7 +21,7 @@ export function createAuthStore() {
} }
isAdmin = !!$user.admin?.global isAdmin = !!$user.admin?.global
isBuilder = !!$user.builder?.global isBuilder = !!$user.builder?.global
tenantId = $user.tenantId || "default" tenantId = $user.tenantId || tenantId
} }
return { return {
user: $user, user: $user,
@ -35,7 +35,6 @@ export function createAuthStore() {
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
checkAuth: async () => { checkAuth: async () => {
const response = await api.get("/api/admin/users/self") const response = await api.get("/api/admin/users/self")
if (response.status !== 200) { if (response.status !== 200) {
user.set(null) user.set(null)
@ -45,8 +44,12 @@ export function createAuthStore() {
} }
}, },
login: async creds => { login: async creds => {
const tenantId = get(store).tenantId const tenantId = creds.tenantId || get(store).tenantId
const response = await api.post(`/api/admin/auth/${tenantId}/login`, creds) delete creds.tenantId
const response = await api.post(
`/api/admin/auth/${tenantId}/login`,
creds
)
const json = await response.json() const json = await response.json()
if (response.status === 200) { if (response.status === 200) {
user.set(json.user) user.set(json.user)
@ -84,10 +87,13 @@ export function createAuthStore() {
}, },
resetPassword: async (password, code) => { resetPassword: async (password, code) => {
const tenantId = get(store).tenantId const tenantId = get(store).tenantId
const response = await api.post(`/api/admin/auth/${tenantId}/reset/update`, { const response = await api.post(
password, `/api/admin/auth/${tenantId}/reset/update`,
resetCode: code, {
}) password,
resetCode: code,
}
)
if (response.status !== 200) { if (response.status !== 200) {
throw "Unable to reset password" throw "Unable to reset password"
} }

View file

@ -1,4 +1,3 @@
const CouchDB = require("../../db")
const { StaticDatabases, getGlobalDBFromCtx } = require("@budibase/auth/db") const { StaticDatabases, getGlobalDBFromCtx } = require("@budibase/auth/db")
const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys
@ -22,7 +21,6 @@ async function setBuilderMainDoc(ctx, doc) {
return db.put(doc) return db.put(doc)
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
try { try {
const mainDoc = await getBuilderMainDoc(ctx) const mainDoc = await getBuilderMainDoc(ctx)

View file

@ -25,7 +25,7 @@ const { BASE_LAYOUTS } = require("../../constants/layouts")
const { createHomeScreen } = require("../../constants/screens") const { createHomeScreen } = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const { processObject } = require("@budibase/string-templates") const { processObject } = require("@budibase/string-templates")
const { getAllApps } = require("../../utilities") const { getAllApps } = require("@budibase/auth/db")
const { USERS_TABLE_SCHEMA } = require("../../constants") const { USERS_TABLE_SCHEMA } = require("../../constants")
const { const {
getDeployedApps, getDeployedApps,
@ -128,7 +128,8 @@ async function createInstance(template) {
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps({ CouchDB, dev, all }) const tenantId = ctx.user.tenantId
const apps = await getAllApps({ tenantId, dev, all })
// get the locks for all the dev apps // get the locks for all the dev apps
if (dev || all) { if (dev || all) {
@ -188,6 +189,7 @@ exports.fetchAppPackage = async function (ctx) {
} }
exports.create = async function (ctx) { exports.create = async function (ctx) {
const tenantId = ctx.user.tenantId
const { useTemplate, templateKey } = ctx.request.body const { useTemplate, templateKey } = ctx.request.body
const instanceConfig = { const instanceConfig = {
useTemplate, useTemplate,
@ -220,6 +222,7 @@ exports.create = async function (ctx) {
url: url, url: url,
template: ctx.request.body.template, template: ctx.request.body.template,
instance: instance, instance: instance,
tenantId,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
} }

View file

@ -151,6 +151,7 @@ exports.create = async function (ctx) {
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
automation.tenantId = ctx.user.tenantId
// call through to update if already exists // call through to update if already exists
if (automation._id && automation._rev) { if (automation._id && automation._rev) {
@ -159,7 +160,6 @@ exports.create = async function (ctx) {
automation._id = generateAutomationID() automation._id = generateAutomationID()
automation.tenantId = ctx.user.tenantId
automation.type = "automation" automation.type = "automation"
automation = cleanAutomationInputs(automation) automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({ automation = await checkForWebhooks({

View file

@ -1,6 +1,5 @@
const env = require("../environment") const env = require("../environment")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants") const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
const { getAllApps } = require("@budibase/auth/db")
const { sanitizeKey } = require("@budibase/auth/src/objectStore") const { sanitizeKey } = require("@budibase/auth/src/objectStore")
const BB_CDN = "https://cdn.app.budi.live/assets" const BB_CDN = "https://cdn.app.budi.live/assets"
@ -8,7 +7,6 @@ const BB_CDN = "https://cdn.app.budi.live/assets"
exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms)) exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms))
exports.isDev = env.isDev exports.isDev = env.isDev
exports.getAllApps = getAllApps
/** /**
* Makes sure that a URL has the correct number of slashes, while maintaining the * Makes sure that a URL has the correct number of slashes, while maintaining the

View file

@ -169,14 +169,14 @@ exports.destroy = async function (ctx) {
} }
exports.configChecklist = async function (ctx) { exports.configChecklist = async function (ctx) {
const tenantId = ctx.query.tenantId const tenantId = ctx.request.query.tenantId
const db = tenantId ? getGlobalDB(tenantId) : getGlobalDBFromCtx(ctx) const db = tenantId ? getGlobalDB(tenantId) : getGlobalDBFromCtx(ctx)
try { try {
// TODO: Watch get started video // TODO: Watch get started video
// Apps exist // Apps exist
const apps = (await getAllApps({ CouchDB })) const apps = await getAllApps({ tenantId })
// They have set up SMTP // They have set up SMTP
const smtpConfig = await getScopedFullConfig(db, { const smtpConfig = await getScopedFullConfig(db, {

View file

@ -2,8 +2,16 @@ const { sendEmail } = require("../../../utilities/email")
const { getGlobalDBFromCtx } = require("@budibase/auth/db") const { getGlobalDBFromCtx } = require("@budibase/auth/db")
exports.sendEmail = async ctx => { exports.sendEmail = async ctx => {
let { tenantId, workspaceId, email, userId, purpose, contents, from, subject } = let {
ctx.request.body tenantId,
workspaceId,
email,
userId,
purpose,
contents,
from,
subject,
} = ctx.request.body
let user let user
if (userId) { if (userId) {
const db = getGlobalDBFromCtx(ctx) const db = getGlobalDBFromCtx(ctx)

View file

@ -7,8 +7,9 @@ const {
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const tenantId = ctx.user.tenantId
// always use the dev apps as they'll be most up to date (true) // always use the dev apps as they'll be most up to date (true)
const apps = await getAllApps({ CouchDB, all: true }) const apps = await getAllApps({ tenantId, all: true })
const promises = [] const promises = []
for (let app of apps) { for (let app of apps) {
// use dev app IDs // use dev app IDs

View file

@ -3,7 +3,7 @@ const {
getGlobalUserParams, getGlobalUserParams,
getGlobalDB, getGlobalDB,
getGlobalDBFromCtx, getGlobalDBFromCtx,
StaticDatabases StaticDatabases,
} = require("@budibase/auth/db") } = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail, newid } = require("@budibase/auth").utils const { hash, getGlobalUserByEmail, newid } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants") const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
@ -16,17 +16,17 @@ const CouchDB = require("../../../db")
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
const tenantDocId = StaticDatabases.PLATFORM_INFO.docs.tenants const tenantDocId = StaticDatabases.PLATFORM_INFO.docs.tenants
async function noTenantsExist() {
const db = new CouchDB(PLATFORM_INFO_DB)
const tenants = await db.get(tenantDocId)
return !tenants || !tenants.tenantIds || tenants.tenantIds.length === 0
}
async function tryAddTenant(tenantId) { async function tryAddTenant(tenantId) {
const db = new CouchDB(PLATFORM_INFO_DB) const db = new CouchDB(PLATFORM_INFO_DB)
let tenants = await db.get(tenantDocId) let tenants
try {
tenants = await db.get(tenantDocId)
} catch (err) {
// if theres an error don't worry, we'll just write it in
}
if (!tenants || !Array.isArray(tenants.tenantIds)) { if (!tenants || !Array.isArray(tenants.tenantIds)) {
tenants = { tenants = {
_id: tenantDocId,
tenantIds: [], tenantIds: [],
} }
} }
@ -120,11 +120,18 @@ exports.save = async ctx => {
} }
exports.adminUser = async ctx => { exports.adminUser = async ctx => {
if (!await noTenantsExist()) { const { email, password, tenantId } = ctx.request.body
const db = getGlobalDB(tenantId)
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
})
)
if (response.rows.some(row => row.doc.admin)) {
ctx.throw(403, "You cannot initialise once an admin user has been created.") ctx.throw(403, "You cannot initialise once an admin user has been created.")
} }
const { email, password } = ctx.request.body
const user = { const user = {
email: email, email: email,
password: password, password: password,
@ -135,9 +142,10 @@ exports.adminUser = async ctx => {
admin: { admin: {
global: true, global: true,
}, },
tenantId,
} }
try { try {
ctx.body = await saveUser(user, newid()) ctx.body = await saveUser(user, tenantId)
} catch (err) { } catch (err) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
} }

View file

@ -1,5 +1,8 @@
const { getWorkspaceParams, generateWorkspaceID, getGlobalDBFromCtx } = const {
require("@budibase/auth/db") getWorkspaceParams,
generateWorkspaceID,
getGlobalDBFromCtx,
} = require("@budibase/auth/db")
exports.save = async function (ctx) { exports.save = async function (ctx) {
const db = getGlobalDBFromCtx(ctx) const db = getGlobalDBFromCtx(ctx)

View file

@ -1,10 +1,10 @@
const { getAllApps } = require("@budibase/auth/db") const { getAllApps } = require("@budibase/auth/db")
const CouchDB = require("../../db")
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
exports.getApps = async ctx => { exports.getApps = async ctx => {
const apps = await getAllApps({ CouchDB }) const tenantId = ctx.user.tenantId
const apps = await getAllApps({ tenantId })
const body = {} const body = {}
for (let app of apps) { for (let app of apps) {

View file

@ -29,8 +29,16 @@ function buildResetUpdateValidation() {
} }
router router
.post("/api/admin/auth/:tenantId/login", buildAuthValidation(), authController.authenticate) .post(
.post("/api/admin/auth/:tenantId/reset", buildResetValidation(), authController.reset) "/api/admin/auth/:tenantId/login",
buildAuthValidation(),
authController.authenticate
)
.post(
"/api/admin/auth/:tenantId/reset",
buildResetValidation(),
authController.reset
)
.post( .post(
"/api/admin/auth/:tenantId/reset/update", "/api/admin/auth/:tenantId/reset/update",
buildResetUpdateValidation(), buildResetUpdateValidation(),

View file

@ -11,6 +11,7 @@ function buildAdminInitValidation() {
Joi.object({ Joi.object({
email: Joi.string().required(), email: Joi.string().required(),
password: Joi.string().required(), password: Joi.string().required(),
tenantId: Joi.string().required(),
}) })
.required() .required()
.unknown(false) .unknown(false)