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",
GOOGLE: "google",
}
exports.DEFAULT_TENANT_ID = "default"

View File

@ -1,10 +1,10 @@
const { newid } = require("../hashing")
const Replication = require("./Replication")
const { getDB } = require("./index")
const { getDB, getCouch } = require("./index")
const { DEFAULT_TENANT_ID } = require("../constants")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
const DEFAULT_TENANT = "default"
exports.ViewNames = {
USER_BY_EMAIL: "by_email",
@ -75,7 +75,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
exports.getGlobalDB = tenantId => {
// fallback for system pre multi-tenancy
let dbName = exports.StaticDatabases.GLOBAL.name
if (tenantId && tenantId !== DEFAULT_TENANT) {
if (tenantId && tenantId !== DEFAULT_TENANT_ID) {
dbName = `${tenantId}${SEPARATOR}${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.
* @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()
const appDbNames = allDbs.filter(dbName =>
dbName.startsWith(exports.APP_PREFIX)
@ -208,8 +212,13 @@ exports.getAllApps = async ({ CouchDB, dev, all } = {}) => {
const apps = response
.filter(result => result.status === "fulfilled" )
.map(({ value }) => value)
.filter(app => {
const appTenant = !app.tenantId ? DEFAULT_TENANT_ID : app.tenantId
return tenantId === appTenant
})
if (!all) {
return apps.filter(app => {
if (dev) {
return isDevApp(app)
}

View File

@ -1,5 +1,5 @@
const jwt = require("jsonwebtoken")
const { UserStatus } = require("../../constants")
const { UserStatus, DEFAULT_TENANT_ID } = require("../../constants")
const { compare } = require("../../hashing")
const env = require("../../environment")
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 (!password) return done(null, false, "Password Required.")
const params = ctx.params || {}
const query = ctx.query || {}
// 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)
if (dbUser == null) {
return done(null, false, { message: "User not found" })
@ -41,7 +40,6 @@ exports.authenticate = async function (ctx, email, password, done) {
// authenticate
if (await compare(password, dbUser.password)) {
const sessionId = newid()
const tenantId = dbUser.tenantId
await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign(

View File

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

View File

@ -15,6 +15,7 @@
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
let tenantId = ""
let username = ""
let password = ""
@ -25,6 +26,7 @@
await auth.login({
username,
password,
tenantId,
})
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) {
@ -64,6 +66,7 @@
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Organisation" bind:value={tenantId} />
<Input label="Email" bind:value={username} />
<Input
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 { auth } from "stores/portal"
export function createAdminStore() {
const { subscribe, set } = writable({})
async function init() {
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 onboardingSteps = Object.keys(json)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
const env = require("../environment")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
const { getAllApps } = require("@budibase/auth/db")
const { sanitizeKey } = require("@budibase/auth/src/objectStore")
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.isDev = env.isDev
exports.getAllApps = getAllApps
/**
* 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) {
const tenantId = ctx.query.tenantId
const tenantId = ctx.request.query.tenantId
const db = tenantId ? getGlobalDB(tenantId) : getGlobalDBFromCtx(ctx)
try {
// TODO: Watch get started video
// Apps exist
const apps = (await getAllApps({ CouchDB }))
const apps = await getAllApps({ tenantId })
// They have set up SMTP
const smtpConfig = await getScopedFullConfig(db, {

View File

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

View File

@ -7,8 +7,9 @@ const {
const CouchDB = require("../../../db")
exports.fetch = async ctx => {
const tenantId = ctx.user.tenantId
// 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 = []
for (let app of apps) {
// use dev app IDs

View File

@ -3,7 +3,7 @@ const {
getGlobalUserParams,
getGlobalDB,
getGlobalDBFromCtx,
StaticDatabases
StaticDatabases,
} = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail, newid } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
@ -16,17 +16,17 @@ const CouchDB = require("../../../db")
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
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) {
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)) {
tenants = {
_id: tenantDocId,
tenantIds: [],
}
}
@ -120,11 +120,18 @@ exports.save = 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.")
}
const { email, password } = ctx.request.body
const user = {
email: email,
password: password,
@ -135,9 +142,10 @@ exports.adminUser = async ctx => {
admin: {
global: true,
},
tenantId,
}
try {
ctx.body = await saveUser(user, newid())
ctx.body = await saveUser(user, tenantId)
} catch (err) {
ctx.throw(err.status || 400, err)
}

View File

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

View File

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

View File

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

View File

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