Revert "Merge pull request #2253 from Budibase/revert-2076-feature/multi-tenants"
This reverts commit4834b765be
, reversing changes made to2456e69483
.
This commit is contained in:
parent
dafe110659
commit
46ea3f3e9e
|
@ -26,10 +26,18 @@ static_resources:
|
||||||
cluster: couchdb-service
|
cluster: couchdb-service
|
||||||
prefix_rewrite: "/"
|
prefix_rewrite: "/"
|
||||||
|
|
||||||
|
- match: { prefix: "/api/system/" }
|
||||||
|
route:
|
||||||
|
cluster: worker-dev
|
||||||
|
|
||||||
- match: { prefix: "/api/admin/" }
|
- match: { prefix: "/api/admin/" }
|
||||||
route:
|
route:
|
||||||
cluster: worker-dev
|
cluster: worker-dev
|
||||||
|
|
||||||
|
- match: { prefix: "/api/global/" }
|
||||||
|
route:
|
||||||
|
cluster: worker-dev
|
||||||
|
|
||||||
- match: { prefix: "/api/" }
|
- match: { prefix: "/api/" }
|
||||||
route:
|
route:
|
||||||
cluster: server-dev
|
cluster: server-dev
|
||||||
|
|
|
@ -37,11 +37,19 @@ static_resources:
|
||||||
route:
|
route:
|
||||||
cluster: app-service
|
cluster: app-service
|
||||||
|
|
||||||
# special case for worker admin API
|
# special cases for worker admin (deprecated), global and system API
|
||||||
|
- match: { prefix: "/api/global/" }
|
||||||
|
route:
|
||||||
|
cluster: worker-service
|
||||||
|
|
||||||
- match: { prefix: "/api/admin/" }
|
- match: { prefix: "/api/admin/" }
|
||||||
route:
|
route:
|
||||||
cluster: worker-service
|
cluster: worker-service
|
||||||
|
|
||||||
|
- match: { prefix: "/api/system/" }
|
||||||
|
route:
|
||||||
|
cluster: worker-service
|
||||||
|
|
||||||
- match: { path: "/" }
|
- match: { path: "/" }
|
||||||
route:
|
route:
|
||||||
cluster: app-service
|
cluster: app-service
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e": "lerna run cy:test",
|
||||||
"test:e2e:ci": "lerna run cy:ci",
|
"test:e2e:ci": "lerna run cy:ci",
|
||||||
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
|
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
|
||||||
"build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -"
|
"build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
|
"multi:enable": "lerna run multi:enable",
|
||||||
|
"multi:disable": "lerna run multi:disable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
module.exports = require("./src/db/utils")
|
module.exports = {
|
||||||
|
...require("./src/db/utils"),
|
||||||
|
...require("./src/db/constants"),
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
"aws-sdk": "^2.901.0",
|
"aws-sdk": "^2.901.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cls-hooked": "^4.2.2",
|
||||||
"ioredis": "^4.27.1",
|
"ioredis": "^4.27.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa-passport": "^4.1.4",
|
"koa-passport": "^4.1.4",
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
const { getDB } = require("../db")
|
|
||||||
const { StaticDatabases } = require("../db/utils")
|
|
||||||
const redis = require("../redis/authRedis")
|
const redis = require("../redis/authRedis")
|
||||||
|
const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy")
|
||||||
|
|
||||||
const EXPIRY_SECONDS = 3600
|
const EXPIRY_SECONDS = 3600
|
||||||
|
|
||||||
exports.getUser = async userId => {
|
exports.getUser = async (userId, tenantId = null) => {
|
||||||
|
if (!tenantId) {
|
||||||
|
try {
|
||||||
|
tenantId = getTenantId()
|
||||||
|
} catch (err) {
|
||||||
|
tenantId = await lookupTenantId(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
const client = await redis.getUserClient()
|
const client = await redis.getUserClient()
|
||||||
// try cache
|
// try cache
|
||||||
let user = await client.get(userId)
|
let user = await client.get(userId)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
|
user = await getGlobalDB(tenantId).get(userId)
|
||||||
client.store(userId, user, EXPIRY_SECONDS)
|
client.store(userId, user, EXPIRY_SECONDS)
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -14,13 +14,14 @@ exports.Headers = {
|
||||||
API_VER: "x-budibase-api-version",
|
API_VER: "x-budibase-api-version",
|
||||||
APP_ID: "x-budibase-app-id",
|
APP_ID: "x-budibase-app-id",
|
||||||
TYPE: "x-budibase-type",
|
TYPE: "x-budibase-type",
|
||||||
|
TENANT_ID: "x-budibase-tenant-id",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.GlobalRoles = {
|
exports.GlobalRoles = {
|
||||||
OWNER: "owner",
|
OWNER: "owner",
|
||||||
ADMIN: "admin",
|
ADMIN: "admin",
|
||||||
BUILDER: "builder",
|
BUILDER: "builder",
|
||||||
GROUP_MANAGER: "group_manager",
|
WORKSPACE_MANAGER: "workspace_manager",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.Configs = {
|
exports.Configs = {
|
||||||
|
@ -31,3 +32,5 @@ exports.Configs = {
|
||||||
OIDC: "oidc",
|
OIDC: "oidc",
|
||||||
OIDC_LOGOS: "logos_oidc",
|
OIDC_LOGOS: "logos_oidc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.DEFAULT_TENANT_ID = "default"
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
exports.SEPARATOR = "_"
|
||||||
|
|
||||||
|
exports.StaticDatabases = {
|
||||||
|
GLOBAL: {
|
||||||
|
name: "global-db",
|
||||||
|
docs: {
|
||||||
|
apiKeys: "apikeys",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// contains information about tenancy and so on
|
||||||
|
PLATFORM_INFO: {
|
||||||
|
name: "global-info",
|
||||||
|
docs: {
|
||||||
|
tenants: "tenants",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,36 +1,36 @@
|
||||||
const { newid } = require("../hashing")
|
const { newid } = require("../hashing")
|
||||||
const Replication = require("./Replication")
|
const Replication = require("./Replication")
|
||||||
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||||
|
const env = require("../environment")
|
||||||
|
const { StaticDatabases, SEPARATOR } = require("./constants")
|
||||||
|
const { getTenantId } = require("../tenancy")
|
||||||
|
|
||||||
const UNICODE_MAX = "\ufff0"
|
const UNICODE_MAX = "\ufff0"
|
||||||
const SEPARATOR = "_"
|
|
||||||
|
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = {
|
exports.StaticDatabases = StaticDatabases
|
||||||
GLOBAL: {
|
|
||||||
name: "global-db",
|
const PRE_APP = "app"
|
||||||
},
|
const PRE_DEV = "dev"
|
||||||
DEPLOYMENTS: {
|
|
||||||
name: "deployments",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentTypes = {
|
const DocumentTypes = {
|
||||||
USER: "us",
|
USER: "us",
|
||||||
GROUP: "group",
|
WORKSPACE: "workspace",
|
||||||
CONFIG: "config",
|
CONFIG: "config",
|
||||||
TEMPLATE: "template",
|
TEMPLATE: "template",
|
||||||
APP: "app",
|
APP: PRE_APP,
|
||||||
APP_DEV: "app_dev",
|
DEV: PRE_DEV,
|
||||||
APP_METADATA: "app_metadata",
|
APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`,
|
||||||
|
APP_METADATA: `${PRE_APP}${SEPARATOR}metadata`,
|
||||||
ROLE: "role",
|
ROLE: "role",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.DocumentTypes = DocumentTypes
|
exports.DocumentTypes = DocumentTypes
|
||||||
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
||||||
function isDevApp(app) {
|
function isDevApp(app) {
|
||||||
|
@ -61,21 +61,21 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new group ID.
|
* Generates a new workspace ID.
|
||||||
* @returns {string} The new group ID which the group doc can be stored under.
|
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||||
*/
|
*/
|
||||||
exports.generateGroupID = () => {
|
exports.generateWorkspaceID = () => {
|
||||||
return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving groups.
|
* Gets parameters for retrieving workspaces.
|
||||||
*/
|
*/
|
||||||
exports.getGroupParams = (id = "", otherProps = {}) => {
|
exports.getWorkspaceParams = (id = "", otherProps = {}) => {
|
||||||
return {
|
return {
|
||||||
...otherProps,
|
...otherProps,
|
||||||
startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`,
|
startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`,
|
||||||
endkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}${UNICODE_MAX}`,
|
endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,14 +103,14 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a template ID.
|
* Generates a template ID.
|
||||||
* @param ownerId The owner/user of the template, this could be global or a group level.
|
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
||||||
*/
|
*/
|
||||||
exports.generateTemplateID = ownerId => {
|
exports.generateTemplateID = ownerId => {
|
||||||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level.
|
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
|
||||||
*/
|
*/
|
||||||
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
|
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
|
||||||
if (!templateId) {
|
if (!templateId) {
|
||||||
|
@ -163,11 +163,26 @@ 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 (CouchDB, { dev, all } = {}) => {
|
||||||
|
let tenantId = getTenantId()
|
||||||
|
if (!env.MULTI_TENANCY && !tenantId) {
|
||||||
|
tenantId = DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
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)
|
const split = dbName.split(SEPARATOR)
|
||||||
|
// it is an app, check the tenantId
|
||||||
|
if (split[0] === DocumentTypes.APP) {
|
||||||
|
const noTenantId = split.length === 2 || split[1] === DocumentTypes.DEV
|
||||||
|
// tenantId is always right before the UUID
|
||||||
|
const possibleTenantId = split[split.length - 2]
|
||||||
|
return (
|
||||||
|
(tenantId === DEFAULT_TENANT_ID && noTenantId) ||
|
||||||
|
possibleTenantId === tenantId
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
const appPromises = appDbNames.map(db =>
|
const appPromises = appDbNames.map(db =>
|
||||||
// skip setup otherwise databases could be re-created
|
// skip setup otherwise databases could be re-created
|
||||||
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
|
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
|
||||||
|
@ -214,8 +229,8 @@ exports.dbExists = async (CouchDB, dbName) => {
|
||||||
* Generates a new configuration ID.
|
* Generates a new configuration ID.
|
||||||
* @returns {string} The new configuration ID which the config doc can be stored under.
|
* @returns {string} The new configuration ID which the config doc can be stored under.
|
||||||
*/
|
*/
|
||||||
const generateConfigID = ({ type, group, user }) => {
|
const generateConfigID = ({ type, workspace, user }) => {
|
||||||
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
|
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
||||||
|
|
||||||
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
|
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
|
||||||
}
|
}
|
||||||
|
@ -223,8 +238,8 @@ const generateConfigID = ({ type, group, user }) => {
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving configurations.
|
* Gets parameters for retrieving configurations.
|
||||||
*/
|
*/
|
||||||
const getConfigParams = ({ type, group, user }, otherProps = {}) => {
|
const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
|
||||||
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
|
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...otherProps,
|
...otherProps,
|
||||||
|
@ -234,15 +249,15 @@ const getConfigParams = ({ type, group, user }, otherProps = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the most granular configuration document from the DB based on the type, group and userID passed.
|
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
||||||
* @param {Object} db - db instance to query
|
* @param {Object} db - db instance to query
|
||||||
* @param {Object} scopes - the type, group and userID scopes of the configuration.
|
* @param {Object} scopes - the type, workspace and userID scopes of the configuration.
|
||||||
* @returns The most granular configuration document based on the scope.
|
* @returns The most granular configuration document based on the scope.
|
||||||
*/
|
*/
|
||||||
const getScopedFullConfig = async function (db, { type, user, group }) {
|
const getScopedFullConfig = async function (db, { type, user, workspace }) {
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getConfigParams(
|
getConfigParams(
|
||||||
{ type, user, group },
|
{ type, user, workspace },
|
||||||
{
|
{
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
}
|
}
|
||||||
|
@ -252,14 +267,14 @@ const getScopedFullConfig = async function (db, { type, user, group }) {
|
||||||
function determineScore(row) {
|
function determineScore(row) {
|
||||||
const config = row.doc
|
const config = row.doc
|
||||||
|
|
||||||
// Config is specific to a user and a group
|
// Config is specific to a user and a workspace
|
||||||
if (config._id.includes(generateConfigID({ type, user, group }))) {
|
if (config._id.includes(generateConfigID({ type, user, workspace }))) {
|
||||||
return 4
|
return 4
|
||||||
} else if (config._id.includes(generateConfigID({ type, user }))) {
|
} else if (config._id.includes(generateConfigID({ type, user }))) {
|
||||||
// Config is specific to a user only
|
// Config is specific to a user only
|
||||||
return 3
|
return 3
|
||||||
} else if (config._id.includes(generateConfigID({ type, group }))) {
|
} else if (config._id.includes(generateConfigID({ type, workspace }))) {
|
||||||
// Config is specific to a group only
|
// Config is specific to a workspace only
|
||||||
return 2
|
return 2
|
||||||
} else if (config._id.includes(generateConfigID({ type }))) {
|
} else if (config._id.includes(generateConfigID({ type }))) {
|
||||||
// Config is specific to a type only
|
// Config is specific to a type only
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils")
|
const { DocumentTypes, ViewNames } = require("./utils")
|
||||||
const { getDB } = require("./index")
|
|
||||||
|
|
||||||
function DesignDoc() {
|
function DesignDoc() {
|
||||||
return {
|
return {
|
||||||
|
@ -10,8 +9,7 @@ function DesignDoc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUserEmailView = async () => {
|
exports.createUserEmailView = async db => {
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get("_design/database")
|
||||||
|
|
|
@ -16,6 +16,7 @@ module.exports = {
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
isTest,
|
isTest,
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
|
|
|
@ -2,6 +2,7 @@ const passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
const JwtStrategy = require("passport-jwt").Strategy
|
||||||
const { StaticDatabases } = require("./db/utils")
|
const { StaticDatabases } = require("./db/utils")
|
||||||
|
const { getGlobalDB } = require("./tenancy")
|
||||||
const {
|
const {
|
||||||
jwt,
|
jwt,
|
||||||
local,
|
local,
|
||||||
|
@ -9,8 +10,9 @@ const {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
auditLog,
|
auditLog,
|
||||||
|
tenancy,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
const { setDB, getDB } = require("./db")
|
const { setDB } = require("./db")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
|
@ -20,7 +22,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
|
||||||
passport.serializeUser((user, done) => done(null, user))
|
passport.serializeUser((user, done) => done(null, user))
|
||||||
|
|
||||||
passport.deserializeUser(async (user, done) => {
|
passport.deserializeUser(async (user, done) => {
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
const db = getGlobalDB()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await db.get(user._id)
|
const user = await db.get(user._id)
|
||||||
|
@ -54,6 +56,7 @@ module.exports = {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
jwt: require("jsonwebtoken"),
|
jwt: require("jsonwebtoken"),
|
||||||
|
buildTenancyMiddleware: tenancy,
|
||||||
auditLog,
|
auditLog,
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
|
|
|
@ -2,46 +2,34 @@ const { Cookies, Headers } = require("../constants")
|
||||||
const { getCookie, clearCookie } = require("../utils")
|
const { getCookie, clearCookie } = require("../utils")
|
||||||
const { getUser } = require("../cache/user")
|
const { getUser } = require("../cache/user")
|
||||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||||
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const PARAM_REGEX = /\/:(.*?)\//g
|
function finalise(
|
||||||
|
ctx,
|
||||||
function buildNoAuthRegex(patterns) {
|
{ authenticated, user, internal, version, publicEndpoint } = {}
|
||||||
return patterns.map(pattern => {
|
) {
|
||||||
const isObj = typeof pattern === "object" && pattern.route
|
ctx.publicEndpoint = publicEndpoint || false
|
||||||
const method = isObj ? pattern.method : "GET"
|
|
||||||
let route = isObj ? pattern.route : pattern
|
|
||||||
|
|
||||||
const matches = route.match(PARAM_REGEX)
|
|
||||||
if (matches) {
|
|
||||||
for (let match of matches) {
|
|
||||||
route = route.replace(match, "/.*/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { regex: new RegExp(route), method }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalise(ctx, { authenticated, user, internal, version } = {}) {
|
|
||||||
ctx.isAuthenticated = authenticated || false
|
ctx.isAuthenticated = authenticated || false
|
||||||
ctx.user = user
|
ctx.user = user
|
||||||
ctx.internal = internal || false
|
ctx.internal = internal || false
|
||||||
ctx.version = version
|
ctx.version = version
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (noAuthPatterns = [], opts) => {
|
/**
|
||||||
const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : []
|
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
|
||||||
|
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
||||||
|
* has not yet been populated.
|
||||||
|
*/
|
||||||
|
module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
|
||||||
|
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
|
let publicEndpoint = false
|
||||||
const version = ctx.request.headers[Headers.API_VER]
|
const version = ctx.request.headers[Headers.API_VER]
|
||||||
// the path is not authenticated
|
// the path is not authenticated
|
||||||
const found = noAuthOptions.find(({ regex, method }) => {
|
const found = matches(ctx, noAuthOptions)
|
||||||
return (
|
if (found) {
|
||||||
regex.test(ctx.request.url) &&
|
publicEndpoint = true
|
||||||
ctx.request.method.toLowerCase() === method.toLowerCase()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
if (found != null) {
|
|
||||||
return next()
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// check the actual user is authenticated first
|
// check the actual user is authenticated first
|
||||||
|
@ -58,7 +46,7 @@ module.exports = (noAuthPatterns = [], opts) => {
|
||||||
error = "No session found"
|
error = "No session found"
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
user = await getUser(userId)
|
user = await getUser(userId, session.tenantId)
|
||||||
delete user.password
|
delete user.password
|
||||||
authenticated = true
|
authenticated = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -75,22 +63,26 @@ module.exports = (noAuthPatterns = [], opts) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
|
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
|
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
|
||||||
authenticated = true
|
authenticated = true
|
||||||
internal = true
|
internal = true
|
||||||
}
|
}
|
||||||
|
if (!user && tenantId) {
|
||||||
|
user = { tenantId }
|
||||||
|
}
|
||||||
// be explicit
|
// be explicit
|
||||||
if (authenticated !== true) {
|
if (authenticated !== true) {
|
||||||
authenticated = false
|
authenticated = false
|
||||||
}
|
}
|
||||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||||
finalise(ctx, { authenticated, user, internal, version })
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||||
return next()
|
return next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// allow configuring for public access
|
// allow configuring for public access
|
||||||
if (opts && opts.publicAllowed) {
|
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
||||||
finalise(ctx, { authenticated: false, version })
|
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const google = require("./passport/google")
|
||||||
const oidc = require("./passport/oidc")
|
const oidc = require("./passport/oidc")
|
||||||
const authenticated = require("./authenticated")
|
const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
|
const tenancy = require("./tenancy")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
|
@ -12,4 +13,5 @@ module.exports = {
|
||||||
local,
|
local,
|
||||||
authenticated,
|
authenticated,
|
||||||
auditLog,
|
auditLog,
|
||||||
|
tenancy,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
|
||||||
|
|
||||||
|
exports.buildMatcherRegex = patterns => {
|
||||||
|
if (!patterns) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return patterns.map(pattern => {
|
||||||
|
const isObj = typeof pattern === "object" && pattern.route
|
||||||
|
const method = isObj ? pattern.method : "GET"
|
||||||
|
let route = isObj ? pattern.route : pattern
|
||||||
|
|
||||||
|
const matches = route.match(PARAM_REGEX)
|
||||||
|
if (matches) {
|
||||||
|
for (let match of matches) {
|
||||||
|
const pattern = "/.*" + (match.endsWith("/") ? "/" : "")
|
||||||
|
route = route.replace(match, pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { regex: new RegExp(route), method }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.matches = (ctx, options) => {
|
||||||
|
return options.find(({ regex, method }) => {
|
||||||
|
const urlMatch = regex.test(ctx.request.url)
|
||||||
|
const methodMatch =
|
||||||
|
method === "ALL"
|
||||||
|
? true
|
||||||
|
: ctx.request.method.toLowerCase() === method.toLowerCase()
|
||||||
|
|
||||||
|
return urlMatch && methodMatch
|
||||||
|
})
|
||||||
|
}
|
|
@ -27,13 +27,13 @@ async function authenticate(accessToken, refreshToken, profile, done) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport Google Strategy
|
* @returns Dynamically configured Passport Google Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (config) {
|
exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret, callbackURL } = config
|
const { clientID, clientSecret } = config
|
||||||
|
|
||||||
if (!clientID || !clientSecret || !callbackURL) {
|
if (!clientID || !clientSecret) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Configuration invalid. Must contain google clientID, clientSecret and callbackURL"
|
"Configuration invalid. Must contain google clientID and clientSecret"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ exports.strategyFactory = async function (config) {
|
||||||
{
|
{
|
||||||
clientID: config.clientID,
|
clientID: config.clientID,
|
||||||
clientSecret: config.clientSecret,
|
clientSecret: config.clientSecret,
|
||||||
callbackURL: config.callbackURL,
|
callbackURL: callbackUrl,
|
||||||
},
|
},
|
||||||
authenticate
|
authenticate
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,19 +6,23 @@ const { getGlobalUserByEmail } = require("../../utils")
|
||||||
const { authError } = require("./utils")
|
const { authError } = require("./utils")
|
||||||
const { newid } = require("../../hashing")
|
const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
|
const { getTenantId } = require("../../tenancy")
|
||||||
|
|
||||||
const INVALID_ERR = "Invalid Credentials"
|
const INVALID_ERR = "Invalid Credentials"
|
||||||
|
|
||||||
exports.options = {}
|
exports.options = {
|
||||||
|
passReqToCallback: true,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passport Local Authentication Middleware.
|
* Passport Local Authentication Middleware.
|
||||||
* @param {*} email - username to login with
|
* @param {*} ctx the request structure
|
||||||
* @param {*} password - plain text password to log in with
|
* @param {*} email username to login with
|
||||||
* @param {*} done - callback from passport to return user information and errors
|
* @param {*} password plain text password to log in with
|
||||||
|
* @param {*} done callback from passport to return user information and errors
|
||||||
* @returns The authenticated user, or errors if they occur
|
* @returns The authenticated user, or errors if they occur
|
||||||
*/
|
*/
|
||||||
exports.authenticate = async function (email, password, done) {
|
exports.authenticate = async function (ctx, email, password, done) {
|
||||||
if (!email) return authError(done, "Email Required")
|
if (!email) return authError(done, "Email Required")
|
||||||
if (!password) return authError(done, "Password Required")
|
if (!password) return authError(done, "Password Required")
|
||||||
|
|
||||||
|
@ -35,12 +39,14 @@ exports.authenticate = async function (email, password, done) {
|
||||||
// authenticate
|
// authenticate
|
||||||
if (await compare(password, dbUser.password)) {
|
if (await compare(password, dbUser.password)) {
|
||||||
const sessionId = newid()
|
const sessionId = newid()
|
||||||
await createASession(dbUser._id, sessionId)
|
const tenantId = getTenantId()
|
||||||
|
await createASession(dbUser._id, { sessionId, tenantId })
|
||||||
|
|
||||||
dbUser.token = jwt.sign(
|
dbUser.token = jwt.sign(
|
||||||
{
|
{
|
||||||
userId: dbUser._id,
|
userId: dbUser._id,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
tenantId,
|
||||||
},
|
},
|
||||||
env.JWT_SECRET
|
env.JWT_SECRET
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
const { data } = require("./utilities/mock-data")
|
const { data } = require("./utilities/mock-data")
|
||||||
|
|
||||||
|
const TENANT_ID = "default"
|
||||||
|
|
||||||
const googleConfig = {
|
const googleConfig = {
|
||||||
callbackURL: "http://somecallbackurl",
|
|
||||||
clientID: data.clientID,
|
clientID: data.clientID,
|
||||||
clientSecret: data.clientSecret,
|
clientSecret: data.clientSecret,
|
||||||
}
|
}
|
||||||
|
@ -27,12 +28,13 @@ describe("google", () => {
|
||||||
it("should create successfully create a google strategy", async () => {
|
it("should create successfully create a google strategy", async () => {
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
|
|
||||||
await google.strategyFactory(googleConfig)
|
const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback`
|
||||||
|
await google.strategyFactory(googleConfig, callbackUrl)
|
||||||
|
|
||||||
const expectedOptions = {
|
const expectedOptions = {
|
||||||
clientID: googleConfig.clientID,
|
clientID: googleConfig.clientID,
|
||||||
clientSecret: googleConfig.clientSecret,
|
clientSecret: googleConfig.clientSecret,
|
||||||
callbackURL: googleConfig.callbackURL,
|
callbackURL: callbackUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(mockStrategy).toHaveBeenCalledWith(
|
expect(mockStrategy).toHaveBeenCalledWith(
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const database = require("../../db")
|
const { generateGlobalUserID } = require("../../db/utils")
|
||||||
const { StaticDatabases, generateGlobalUserID } = require("../../db/utils")
|
|
||||||
const { authError } = require("./utils")
|
const { authError } = require("./utils")
|
||||||
const { newid } = require("../../hashing")
|
const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
const { getGlobalUserByEmail } = require("../../utils")
|
const { getGlobalUserByEmail } = require("../../utils")
|
||||||
|
const { getGlobalDB, getTenantId } = require("../../tenancy")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common authentication logic for third parties. e.g. OAuth, OIDC.
|
* Common authentication logic for third parties. e.g. OAuth, OIDC.
|
||||||
|
@ -15,19 +15,21 @@ exports.authenticateThirdParty = async function (
|
||||||
requireLocalAccount = true,
|
requireLocalAccount = true,
|
||||||
done
|
done
|
||||||
) {
|
) {
|
||||||
if (!thirdPartyUser.provider)
|
if (!thirdPartyUser.provider) {
|
||||||
return authError(done, "third party user provider required")
|
return authError(done, "third party user provider required")
|
||||||
if (!thirdPartyUser.userId)
|
}
|
||||||
|
if (!thirdPartyUser.userId) {
|
||||||
return authError(done, "third party user id required")
|
return authError(done, "third party user id required")
|
||||||
if (!thirdPartyUser.email)
|
}
|
||||||
|
if (!thirdPartyUser.email) {
|
||||||
return authError(done, "third party user email required")
|
return authError(done, "third party user email required")
|
||||||
|
}
|
||||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
|
||||||
|
|
||||||
let dbUser
|
|
||||||
|
|
||||||
// use the third party id
|
// use the third party id
|
||||||
const userId = generateGlobalUserID(thirdPartyUser.userId)
|
const userId = generateGlobalUserID(thirdPartyUser.userId)
|
||||||
|
const db = getGlobalDB()
|
||||||
|
|
||||||
|
let dbUser
|
||||||
|
|
||||||
// try to load by id
|
// try to load by id
|
||||||
try {
|
try {
|
||||||
|
@ -73,7 +75,8 @@ exports.authenticateThirdParty = async function (
|
||||||
|
|
||||||
// authenticate
|
// authenticate
|
||||||
const sessionId = newid()
|
const sessionId = newid()
|
||||||
await createASession(dbUser._id, sessionId)
|
const tenantId = getTenantId()
|
||||||
|
await createASession(dbUser._id, { sessionId, tenantId })
|
||||||
|
|
||||||
dbUser.token = jwt.sign(
|
dbUser.token = jwt.sign(
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
const { setTenantId } = require("../tenancy")
|
||||||
|
const ContextFactory = require("../tenancy/FunctionContext")
|
||||||
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
|
|
||||||
|
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => {
|
||||||
|
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
||||||
|
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
|
||||||
|
|
||||||
|
return ContextFactory.getMiddleware(ctx => {
|
||||||
|
const allowNoTenant = !!matches(ctx, noTenancyOptions)
|
||||||
|
const allowQs = !!matches(ctx, allowQsOptions)
|
||||||
|
setTenantId(ctx, { allowQs, allowNoTenant })
|
||||||
|
})
|
||||||
|
}
|
|
@ -12,12 +12,13 @@ function makeSessionID(userId, sessionId) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createASession = async (userId, sessionId) => {
|
exports.createASession = async (userId, session) => {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const session = {
|
const sessionId = session.sessionId
|
||||||
|
session = {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastAccessedAt: new Date().toISOString(),
|
lastAccessedAt: new Date().toISOString(),
|
||||||
sessionId,
|
...session,
|
||||||
userId,
|
userId,
|
||||||
}
|
}
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
const cls = require("cls-hooked")
|
||||||
|
const { newid } = require("../hashing")
|
||||||
|
|
||||||
|
const REQUEST_ID_KEY = "requestId"
|
||||||
|
|
||||||
|
class FunctionContext {
|
||||||
|
static getMiddleware(updateCtxFn = null) {
|
||||||
|
const namespace = this.createNamespace()
|
||||||
|
|
||||||
|
return async function (ctx, next) {
|
||||||
|
await new Promise(
|
||||||
|
namespace.bind(function (resolve, reject) {
|
||||||
|
// store a contextual request ID that can be used anywhere (audit logs)
|
||||||
|
namespace.set(REQUEST_ID_KEY, newid())
|
||||||
|
namespace.bindEmitter(ctx.req)
|
||||||
|
namespace.bindEmitter(ctx.res)
|
||||||
|
|
||||||
|
if (updateCtxFn) {
|
||||||
|
updateCtxFn(ctx)
|
||||||
|
}
|
||||||
|
next().then(resolve).catch(reject)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static run(callback) {
|
||||||
|
const namespace = this.createNamespace()
|
||||||
|
|
||||||
|
return namespace.runAndReturn(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
static setOnContext(key, value) {
|
||||||
|
const namespace = this.createNamespace()
|
||||||
|
namespace.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getContextStorage() {
|
||||||
|
if (this._namespace && this._namespace.active) {
|
||||||
|
let contextData = this._namespace.active
|
||||||
|
delete contextData.id
|
||||||
|
delete contextData._ns_name
|
||||||
|
return contextData
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromContext(key) {
|
||||||
|
const context = this.getContextStorage()
|
||||||
|
if (context) {
|
||||||
|
return context[key]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static destroyNamespace() {
|
||||||
|
if (this._namespace) {
|
||||||
|
cls.destroyNamespace("session")
|
||||||
|
this._namespace = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static createNamespace() {
|
||||||
|
if (!this._namespace) {
|
||||||
|
this._namespace = cls.createNamespace("session")
|
||||||
|
}
|
||||||
|
return this._namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FunctionContext
|
|
@ -0,0 +1,81 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const { Headers } = require("../../constants")
|
||||||
|
const cls = require("./FunctionContext")
|
||||||
|
|
||||||
|
exports.DEFAULT_TENANT_ID = "default"
|
||||||
|
|
||||||
|
exports.isDefaultTenant = () => {
|
||||||
|
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isMultiTenant = () => {
|
||||||
|
return env.MULTI_TENANCY
|
||||||
|
}
|
||||||
|
|
||||||
|
const TENANT_ID = "tenantId"
|
||||||
|
|
||||||
|
// used for automations, API endpoints should always be in context already
|
||||||
|
exports.doInTenant = (tenantId, task) => {
|
||||||
|
return cls.run(() => {
|
||||||
|
// set the tenant id
|
||||||
|
cls.setOnContext(TENANT_ID, tenantId)
|
||||||
|
|
||||||
|
// invoke the task
|
||||||
|
const result = task()
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateTenantId = tenantId => {
|
||||||
|
cls.setOnContext(TENANT_ID, tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setTenantId = (
|
||||||
|
ctx,
|
||||||
|
opts = { allowQs: false, allowNoTenant: false }
|
||||||
|
) => {
|
||||||
|
let tenantId
|
||||||
|
// exit early if not multi-tenant
|
||||||
|
if (!exports.isMultiTenant()) {
|
||||||
|
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowQs = opts && opts.allowQs
|
||||||
|
const allowNoTenant = opts && opts.allowNoTenant
|
||||||
|
const header = ctx.request.headers[Headers.TENANT_ID]
|
||||||
|
const user = ctx.user || {}
|
||||||
|
if (allowQs) {
|
||||||
|
const query = ctx.request.query || {}
|
||||||
|
tenantId = query.tenantId
|
||||||
|
}
|
||||||
|
// override query string (if allowed) by user, or header
|
||||||
|
// URL params cannot be used in a middleware, as they are
|
||||||
|
// processed later in the chain
|
||||||
|
tenantId = user.tenantId || header || tenantId
|
||||||
|
|
||||||
|
if (!tenantId && !allowNoTenant) {
|
||||||
|
ctx.throw(403, "Tenant id not set")
|
||||||
|
}
|
||||||
|
// check tenant ID just incase no tenant was allowed
|
||||||
|
if (tenantId) {
|
||||||
|
cls.setOnContext(TENANT_ID, tenantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isTenantIdSet = () => {
|
||||||
|
const tenantId = cls.getFromContext(TENANT_ID)
|
||||||
|
return !!tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTenantId = () => {
|
||||||
|
if (!exports.isMultiTenant()) {
|
||||||
|
return exports.DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
const tenantId = cls.getFromContext(TENANT_ID)
|
||||||
|
if (!tenantId) {
|
||||||
|
throw Error("Tenant id not found")
|
||||||
|
}
|
||||||
|
return tenantId
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
...require("./context"),
|
||||||
|
...require("./tenancy"),
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
const { getDB } = require("../db")
|
||||||
|
const { SEPARATOR, StaticDatabases } = require("../db/constants")
|
||||||
|
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||||
|
|
||||||
|
exports.addTenantToUrl = url => {
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
|
||||||
|
if (isMultiTenant()) {
|
||||||
|
const char = url.indexOf("?") === -1 ? "?" : "&"
|
||||||
|
url += `${char}tenantId=${tenantId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doesTenantExist = async tenantId => {
|
||||||
|
const db = getDB(PLATFORM_INFO_DB)
|
||||||
|
let tenants
|
||||||
|
try {
|
||||||
|
tenants = await db.get(TENANT_DOC)
|
||||||
|
} catch (err) {
|
||||||
|
// if theres an error the doc doesn't exist, no tenants exist
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
tenants &&
|
||||||
|
Array.isArray(tenants.tenantIds) &&
|
||||||
|
tenants.tenantIds.indexOf(tenantId) !== -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.tryAddTenant = async (tenantId, userId, email) => {
|
||||||
|
const db = getDB(PLATFORM_INFO_DB)
|
||||||
|
const getDoc = async id => {
|
||||||
|
if (!id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await db.get(id)
|
||||||
|
} catch (err) {
|
||||||
|
return { _id: id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let [tenants, userIdDoc, emailDoc] = await Promise.all([
|
||||||
|
getDoc(TENANT_DOC),
|
||||||
|
getDoc(userId),
|
||||||
|
getDoc(email),
|
||||||
|
])
|
||||||
|
if (!Array.isArray(tenants.tenantIds)) {
|
||||||
|
tenants = {
|
||||||
|
_id: TENANT_DOC,
|
||||||
|
tenantIds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let promises = []
|
||||||
|
if (userIdDoc) {
|
||||||
|
userIdDoc.tenantId = tenantId
|
||||||
|
promises.push(db.put(userIdDoc))
|
||||||
|
}
|
||||||
|
if (emailDoc) {
|
||||||
|
emailDoc.tenantId = tenantId
|
||||||
|
promises.push(db.put(emailDoc))
|
||||||
|
}
|
||||||
|
if (tenants.tenantIds.indexOf(tenantId) === -1) {
|
||||||
|
tenants.tenantIds.push(tenantId)
|
||||||
|
promises.push(db.put(tenants))
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getGlobalDB = (tenantId = null) => {
|
||||||
|
// tenant ID can be set externally, for example user API where
|
||||||
|
// new tenants are being created, this may be the case
|
||||||
|
if (!tenantId) {
|
||||||
|
tenantId = getTenantId()
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbName
|
||||||
|
|
||||||
|
if (tenantId === DEFAULT_TENANT_ID) {
|
||||||
|
dbName = StaticDatabases.GLOBAL.name
|
||||||
|
} else {
|
||||||
|
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDB(dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.lookupTenantId = async userId => {
|
||||||
|
const db = getDB(StaticDatabases.PLATFORM_INFO.name)
|
||||||
|
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null
|
||||||
|
try {
|
||||||
|
const doc = await db.get(userId)
|
||||||
|
if (doc && doc.tenantId) {
|
||||||
|
tenantId = doc.tenantId
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// just return the default
|
||||||
|
}
|
||||||
|
return tenantId
|
||||||
|
}
|
|
@ -1,14 +1,9 @@
|
||||||
const {
|
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
|
||||||
DocumentTypes,
|
|
||||||
SEPARATOR,
|
|
||||||
ViewNames,
|
|
||||||
StaticDatabases,
|
|
||||||
} = require("./db/utils")
|
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
const { createUserEmailView } = require("./db/views")
|
const { createUserEmailView } = require("./db/views")
|
||||||
const { getDB } = require("./db")
|
|
||||||
const { Headers } = require("./constants")
|
const { Headers } = require("./constants")
|
||||||
|
const { getGlobalDB } = require("./tenancy")
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
|
||||||
|
@ -111,7 +106,7 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
const db = getGlobalDB()
|
||||||
try {
|
try {
|
||||||
let users = (
|
let users = (
|
||||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
||||||
|
@ -123,7 +118,7 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
return users.length <= 1 ? users[0] : users
|
return users.length <= 1 ? users[0] : users
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err != null && err.name === "not_found") {
|
if (err != null && err.name === "not_found") {
|
||||||
await createUserEmailView()
|
await createUserEmailView(db)
|
||||||
return exports.getGlobalUserByEmail(email)
|
return exports.getGlobalUserByEmail(email)
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/tenancy")
|
|
@ -798,6 +798,13 @@ ast-types@0.9.6:
|
||||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
|
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
|
||||||
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
|
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
|
||||||
|
|
||||||
|
async-hook-jl@^1.7.6:
|
||||||
|
version "1.7.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
|
||||||
|
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
|
||||||
|
dependencies:
|
||||||
|
stack-chain "^1.3.7"
|
||||||
|
|
||||||
async@~2.1.4:
|
async@~2.1.4:
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
|
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
|
||||||
|
@ -1144,6 +1151,15 @@ clone-buffer@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
||||||
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
|
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
|
||||||
|
|
||||||
|
cls-hooked@^4.2.2:
|
||||||
|
version "4.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
|
||||||
|
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
|
||||||
|
dependencies:
|
||||||
|
async-hook-jl "^1.7.6"
|
||||||
|
emitter-listener "^1.0.1"
|
||||||
|
semver "^5.4.1"
|
||||||
|
|
||||||
cluster-key-slot@^1.1.0:
|
cluster-key-slot@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
||||||
|
@ -1444,6 +1460,13 @@ electron-to-chromium@^1.3.723:
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082"
|
||||||
integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==
|
integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==
|
||||||
|
|
||||||
|
emitter-listener@^1.0.1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
|
||||||
|
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
|
||||||
|
dependencies:
|
||||||
|
shimmer "^1.2.0"
|
||||||
|
|
||||||
emittery@^0.7.1:
|
emittery@^0.7.1:
|
||||||
version "0.7.2"
|
version "0.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
|
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
|
||||||
|
@ -4035,7 +4058,7 @@ saxes@^5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
xmlchars "^2.2.0"
|
xmlchars "^2.2.0"
|
||||||
|
|
||||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
|
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
|
||||||
version "5.7.1"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
@ -4096,6 +4119,11 @@ shellwords@^0.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||||
|
|
||||||
|
shimmer@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
|
||||||
|
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
|
||||||
|
|
||||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
|
@ -4250,6 +4278,11 @@ sshpk@^1.7.0:
|
||||||
safer-buffer "^2.0.2"
|
safer-buffer "^2.0.2"
|
||||||
tweetnacl "~0.14.0"
|
tweetnacl "~0.14.0"
|
||||||
|
|
||||||
|
stack-chain@^1.3.7:
|
||||||
|
version "1.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
|
||||||
|
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
|
||||||
|
|
||||||
stack-utils@^2.0.2:
|
stack-utils@^2.0.2:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
|
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
||||||
|
$: tenantSet = $auth.tenantSet
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await admin.init()
|
await admin.init()
|
||||||
|
@ -12,9 +15,14 @@
|
||||||
loaded = true
|
loaded = true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Force creation of an admin user if one doesn't exist
|
|
||||||
$: {
|
$: {
|
||||||
if (loaded && !hasAdminUser) {
|
const apiReady = $admin.loaded && $auth.loaded
|
||||||
|
// if tenant is not set go to it
|
||||||
|
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
|
||||||
|
$redirect("./auth/org")
|
||||||
|
}
|
||||||
|
// Force creation of an admin user if one doesn't exist
|
||||||
|
else if (loaded && apiReady && !hasAdminUser) {
|
||||||
$redirect("./admin")
|
$redirect("./admin")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +37,7 @@
|
||||||
!$isActive("./invite")
|
!$isActive("./invite")
|
||||||
) {
|
) {
|
||||||
const returnUrl = encodeURIComponent(window.location.pathname)
|
const returnUrl = encodeURIComponent(window.location.pathname)
|
||||||
$redirect("./auth/login?", { returnUrl })
|
$redirect("./auth?", { returnUrl })
|
||||||
} else if ($auth?.user?.forceResetPassword) {
|
} else if ($auth?.user?.forceResetPassword) {
|
||||||
$redirect("./auth/reset")
|
$redirect("./auth/reset")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,25 @@
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
Body,
|
Body,
|
||||||
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { admin } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
|
||||||
let adminUser = {}
|
let adminUser = {}
|
||||||
let error
|
let error
|
||||||
|
|
||||||
|
$: tenantId = $auth.tenantId
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
|
adminUser.tenantId = tenantId
|
||||||
// Save the admin user
|
// Save the admin user
|
||||||
const response = await api.post(`/api/admin/users/init`, adminUser)
|
const response = await api.post(`/api/global/users/init`, adminUser)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error(json.message)
|
throw new Error(json.message)
|
||||||
|
@ -47,9 +52,22 @@
|
||||||
<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>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
<Button cta disabled={error} on:click={save}>
|
<Button cta disabled={error} on:click={save}>
|
||||||
Create super admin user
|
Create super admin user
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if multiTenancyEnabled}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
admin.unload()
|
||||||
|
$goto("../auth/org")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change organisation
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import GoogleLogo from "assets/google-logo.png"
|
import GoogleLogo from "assets/google-logo.png"
|
||||||
import { organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
|
|
||||||
|
let show
|
||||||
|
|
||||||
|
$: tenantId = $auth.tenantId
|
||||||
$: show = $organisation.google
|
$: show = $organisation.google
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() => window.open("/api/admin/auth/google", "_blank")}
|
on:click={() =>
|
||||||
|
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<img src={GoogleLogo} alt="google icon" />
|
<img src={GoogleLogo} alt="google icon" />
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
window.open(`/api/admin/auth/oidc/configs/${$oidc.uuid}`, "_blank")}
|
window.open(`/api/global/auth/oidc/configs/${$oidc.uuid}`, "_blank")}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<img {src} alt="oidc icon" />
|
<img {src} alt="oidc icon" />
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
Heading,
|
Heading,
|
||||||
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation, auth } from "stores/portal"
|
import { organisation, auth } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
let email = ""
|
let email = ""
|
||||||
|
|
||||||
|
@ -41,9 +43,12 @@
|
||||||
</Body>
|
</Body>
|
||||||
<Input label="Email" bind:value={email} />
|
<Input label="Email" bind:value={email} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<Layout gap="XS" nopadding>
|
||||||
<Button cta on:click={forgot} disabled={!email}>
|
<Button cta on:click={forgot} disabled={!email}>
|
||||||
Reset your password
|
Reset your password
|
||||||
</Button>
|
</Button>
|
||||||
|
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
|
||||||
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
$: tenantSet = $auth.tenantSet
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (loaded && multiTenancyEnabled && !tenantSet) {
|
||||||
|
$redirect("./org")
|
||||||
|
} else if (loaded) {
|
||||||
$redirect("./login")
|
$redirect("./login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await admin.init()
|
||||||
|
await auth.checkQueryString()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { auth, organisation, oidc } from "stores/portal"
|
import { auth, organisation, oidc, admin } from "stores/portal"
|
||||||
import GoogleButton from "./_components/GoogleButton.svelte"
|
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||||
import OIDCButton from "./_components/OIDCButton.svelte"
|
import OIDCButton from "./_components/OIDCButton.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
@ -18,8 +18,10 @@
|
||||||
|
|
||||||
let username = ""
|
let username = ""
|
||||||
let password = ""
|
let password = ""
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
try {
|
try {
|
||||||
|
@ -27,7 +29,6 @@
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
notifications.success("Logged in successfully")
|
|
||||||
if ($auth?.user?.forceResetPassword) {
|
if ($auth?.user?.forceResetPassword) {
|
||||||
$goto("./reset")
|
$goto("./reset")
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
loaded = true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -61,8 +63,10 @@
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<Heading>Sign in to {company}</Heading>
|
<Heading>Sign in to {company}</Heading>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if loaded}
|
||||||
<GoogleButton />
|
<GoogleButton />
|
||||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||||
|
{/if}
|
||||||
<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>
|
||||||
|
@ -79,6 +83,17 @@
|
||||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
{#if multiTenancyEnabled}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
admin.unload()
|
||||||
|
$goto("./org")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change organisation
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import { Body, Button, Divider, Heading, Input, Layout } from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
|
async function setOrg() {
|
||||||
|
if (tenantId == null || tenantId === "") {
|
||||||
|
tenantId = "default"
|
||||||
|
}
|
||||||
|
await auth.setOrg(tenantId)
|
||||||
|
// re-init now org selected
|
||||||
|
await admin.init()
|
||||||
|
$goto("../")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(evt) {
|
||||||
|
if (evt.key === "Enter") setOrg()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await auth.checkQueryString()
|
||||||
|
if (!multiTenancyEnabled) {
|
||||||
|
$goto("../")
|
||||||
|
} else {
|
||||||
|
admin.unload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
<div class="login">
|
||||||
|
<div class="main">
|
||||||
|
<Layout>
|
||||||
|
<Layout noPadding justifyItems="center">
|
||||||
|
<img alt="logo" src={Logo} />
|
||||||
|
<Heading>Set Budibase organisation</Heading>
|
||||||
|
</Layout>
|
||||||
|
<Divider noGrid />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Body size="S" textAlign="center">Set organisation</Body>
|
||||||
|
<Input label="Organisation" bind:value={tenantId} />
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Button cta on:click={setOrg}>Set organisation</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>
|
|
@ -2,13 +2,15 @@
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
auth.checkQueryString()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!$auth.user) {
|
if (!$auth.user) {
|
||||||
$redirect("./auth/login")
|
$redirect(`./auth`)
|
||||||
} else if ($auth.user.builder?.global) {
|
} else if ($auth.user.builder?.global) {
|
||||||
$redirect("./portal")
|
$redirect(`./portal`)
|
||||||
} else {
|
} else {
|
||||||
$redirect("./apps")
|
$redirect(`./apps`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import OneLoginLogo from "assets/onelogin-logo.png"
|
import OneLoginLogo from "assets/onelogin-logo.png"
|
||||||
import OidcLogoPng from "assets/oidc-logo.png"
|
import OidcLogoPng from "assets/oidc-logo.png"
|
||||||
import { isEqual, cloneDeep } from "lodash/fp"
|
import { isEqual, cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
|
@ -22,36 +21,51 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { organisation } from "stores/portal"
|
import { organisation, auth, admin } from "stores/portal"
|
||||||
import { uuid } from "builderStore/uuid"
|
import { uuid } from "builderStore/uuid"
|
||||||
|
|
||||||
|
$: tenantId = $auth.tenantId
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
||||||
Google: "google",
|
Google: "google",
|
||||||
OIDC: "oidc",
|
OIDC: "oidc",
|
||||||
// Github: "github",
|
|
||||||
// AzureAD: "ad",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GoogleConfigFields = {
|
function callbackUrl(tenantId, end) {
|
||||||
Google: ["clientID", "clientSecret", "callbackURL"],
|
let url = `/api/global/auth`
|
||||||
|
if (multiTenancyEnabled && tenantId) {
|
||||||
|
url += `/${tenantId}`
|
||||||
}
|
}
|
||||||
const GoogleConfigLabels = {
|
url += end
|
||||||
Google: {
|
return url
|
||||||
clientID: "Client ID",
|
|
||||||
clientSecret: "Client secret",
|
|
||||||
callbackURL: "Callback URL",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OIDCConfigFields = {
|
$: GoogleConfigFields = {
|
||||||
Oidc: ["configUrl", "clientID", "clientSecret"],
|
Google: [
|
||||||
}
|
{ name: "clientID", label: "Client ID" },
|
||||||
const OIDCConfigLabels = {
|
{ name: "clientSecret", label: "Client secret" },
|
||||||
Oidc: {
|
{
|
||||||
configUrl: "Config URL",
|
name: "callbackURL",
|
||||||
clientID: "Client ID",
|
label: "Callback URL",
|
||||||
clientSecret: "Client Secret",
|
readonly: true,
|
||||||
|
placeholder: callbackUrl(tenantId, "/google/callback"),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
$: OIDCConfigFields = {
|
||||||
|
Oidc: [
|
||||||
|
{ name: "configUrl", label: "Config URL" },
|
||||||
|
{ name: "clientID", label: "Client ID" },
|
||||||
|
{ name: "clientSecret", label: "Client Secret" },
|
||||||
|
{
|
||||||
|
name: "callbackURL",
|
||||||
|
label: "Callback URL",
|
||||||
|
readonly: true,
|
||||||
|
placeholder: callbackUrl(tenantId, "/oidc/callback"),
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
let iconDropdownOptions = [
|
let iconDropdownOptions = [
|
||||||
|
@ -109,17 +123,13 @@
|
||||||
|
|
||||||
// Create a flag so that it will only try to save completed forms
|
// Create a flag so that it will only try to save completed forms
|
||||||
$: partialGoogle =
|
$: partialGoogle =
|
||||||
providers.google?.config?.clientID ||
|
providers.google?.config?.clientID || providers.google?.config?.clientSecret
|
||||||
providers.google?.config?.clientSecret ||
|
|
||||||
providers.google?.config?.callbackURL
|
|
||||||
$: partialOidc =
|
$: partialOidc =
|
||||||
providers.oidc?.config?.configs[0].configUrl ||
|
providers.oidc?.config?.configs[0].configUrl ||
|
||||||
providers.oidc?.config?.configs[0].clientID ||
|
providers.oidc?.config?.configs[0].clientID ||
|
||||||
providers.oidc?.config?.configs[0].clientSecret
|
providers.oidc?.config?.configs[0].clientSecret
|
||||||
$: googleComplete =
|
$: googleComplete =
|
||||||
providers.google?.config?.clientID &&
|
providers.google?.config?.clientID && providers.google?.config?.clientSecret
|
||||||
providers.google?.config?.clientSecret &&
|
|
||||||
providers.google?.config?.callbackURL
|
|
||||||
$: oidcComplete =
|
$: oidcComplete =
|
||||||
providers.oidc?.config?.configs[0].configUrl &&
|
providers.oidc?.config?.configs[0].configUrl &&
|
||||||
providers.oidc?.config?.configs[0].clientID &&
|
providers.oidc?.config?.configs[0].clientID &&
|
||||||
|
@ -129,7 +139,7 @@
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("file", file)
|
data.append("file", file)
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
`/api/admin/configs/upload/logos_oidc/${file.name}`,
|
`/api/global/configs/upload/logos_oidc/${file.name}`,
|
||||||
data,
|
data,
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
@ -149,17 +159,21 @@
|
||||||
let calls = []
|
let calls = []
|
||||||
docs.forEach(element => {
|
docs.forEach(element => {
|
||||||
if (element.type === ConfigTypes.OIDC) {
|
if (element.type === ConfigTypes.OIDC) {
|
||||||
//Add a UUID here so each config is distinguishable when it arrives at the login page.
|
//Add a UUID here so each config is distinguishable when it arrives at the login page
|
||||||
element.config.configs.forEach(config => {
|
for (let config of element.config.configs) {
|
||||||
!config.uuid && (config.uuid = uuid())
|
if (!config.uuid) {
|
||||||
})
|
config.uuid = uuid()
|
||||||
|
}
|
||||||
|
// callback urls shouldn't be included
|
||||||
|
delete config.callbackURL
|
||||||
|
}
|
||||||
if (partialOidc) {
|
if (partialOidc) {
|
||||||
if (!oidcComplete) {
|
if (!oidcComplete) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Please fill in all required ${ConfigTypes.OIDC} fields`
|
`Please fill in all required ${ConfigTypes.OIDC} fields`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
calls.push(api.post(`/api/admin/configs`, element))
|
calls.push(api.post(`/api/global/configs`, element))
|
||||||
// turn the save button grey when clicked
|
// turn the save button grey when clicked
|
||||||
oidcSaveButtonDisabled = true
|
oidcSaveButtonDisabled = true
|
||||||
originalOidcDoc = cloneDeep(providers.oidc)
|
originalOidcDoc = cloneDeep(providers.oidc)
|
||||||
|
@ -173,7 +187,8 @@
|
||||||
`Please fill in all required ${ConfigTypes.Google} fields`
|
`Please fill in all required ${ConfigTypes.Google} fields`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
calls.push(api.post(`/api/admin/configs`, element))
|
delete element.config.callbackURL
|
||||||
|
calls.push(api.post(`/api/global/configs`, element))
|
||||||
googleSaveButtonDisabled = true
|
googleSaveButtonDisabled = true
|
||||||
originalGoogleDoc = cloneDeep(providers.google)
|
originalGoogleDoc = cloneDeep(providers.google)
|
||||||
}
|
}
|
||||||
|
@ -206,7 +221,7 @@
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
// fetch the configs for oauth
|
// fetch the configs for oauth
|
||||||
const googleResponse = await api.get(
|
const googleResponse = await api.get(
|
||||||
`/api/admin/configs/${ConfigTypes.Google}`
|
`/api/global/configs/${ConfigTypes.Google}`
|
||||||
)
|
)
|
||||||
const googleDoc = await googleResponse.json()
|
const googleDoc = await googleResponse.json()
|
||||||
|
|
||||||
|
@ -227,7 +242,7 @@
|
||||||
|
|
||||||
//Get the list of user uploaded logos and push it to the dropdown options.
|
//Get the list of user uploaded logos and push it to the dropdown options.
|
||||||
//This needs to be done before the config call so they're available when the dropdown renders
|
//This needs to be done before the config call so they're available when the dropdown renders
|
||||||
const res = await api.get(`/api/admin/configs/logos_oidc`)
|
const res = await api.get(`/api/global/configs/logos_oidc`)
|
||||||
const configSettings = await res.json()
|
const configSettings = await res.json()
|
||||||
|
|
||||||
if (configSettings.config) {
|
if (configSettings.config) {
|
||||||
|
@ -242,17 +257,16 @@
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const oidcResponse = await api.get(`/api/admin/configs/${ConfigTypes.OIDC}`)
|
const oidcResponse = await api.get(
|
||||||
|
`/api/global/configs/${ConfigTypes.OIDC}`
|
||||||
|
)
|
||||||
const oidcDoc = await oidcResponse.json()
|
const oidcDoc = await oidcResponse.json()
|
||||||
if (!oidcDoc._id) {
|
if (!oidcDoc._id) {
|
||||||
console.log("hi")
|
|
||||||
|
|
||||||
providers.oidc = {
|
providers.oidc = {
|
||||||
type: ConfigTypes.OIDC,
|
type: ConfigTypes.OIDC,
|
||||||
config: { configs: [{ activated: true }] },
|
config: { configs: [{ activated: true }] },
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("hello")
|
|
||||||
originalOidcDoc = cloneDeep(oidcDoc)
|
originalOidcDoc = cloneDeep(oidcDoc)
|
||||||
providers.oidc = oidcDoc
|
providers.oidc = oidcDoc
|
||||||
}
|
}
|
||||||
|
@ -295,8 +309,12 @@
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
{#each GoogleConfigFields.Google as field}
|
{#each GoogleConfigFields.Google as field}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="L">{GoogleConfigLabels.Google[field]}</Label>
|
<Label size="L">{field.label}</Label>
|
||||||
<Input bind:value={providers.google.config[field]} />
|
<Input
|
||||||
|
bind:value={providers.google.config[field.name]}
|
||||||
|
readonly={field.readonly}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
@ -335,14 +353,14 @@
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
{#each OIDCConfigFields.Oidc as field}
|
{#each OIDCConfigFields.Oidc as field}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="L">{OIDCConfigLabels.Oidc[field]}</Label>
|
<Label size="L">{field.label}</Label>
|
||||||
<Input bind:value={providers.oidc.config.configs[0][field]} />
|
<Input
|
||||||
|
bind:value={providers.oidc.config.configs[0][field.name]}
|
||||||
|
readonly={field.readonly}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="form-row">
|
|
||||||
<Label size="L">Callback URL</Label>
|
|
||||||
<Input readonly placeholder="/api/admin/auth/oidc/callback" />
|
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
To customize your login button, fill out the fields below.
|
To customize your login button, fill out the fields below.
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
delete smtp.config.auth
|
delete smtp.config.auth
|
||||||
}
|
}
|
||||||
// Save your SMTP config
|
// Save your SMTP config
|
||||||
const response = await api.post(`/api/admin/configs`, smtp)
|
const response = await api.post(`/api/global/configs`, smtp)
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
const error = await response.text()
|
const error = await response.text()
|
||||||
|
@ -75,7 +75,9 @@
|
||||||
async function fetchSmtp() {
|
async function fetchSmtp() {
|
||||||
loading = true
|
loading = true
|
||||||
// fetch the configs for smtp
|
// fetch the configs for smtp
|
||||||
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
|
const smtpResponse = await api.get(
|
||||||
|
`/api/global/configs/${ConfigTypes.SMTP}`
|
||||||
|
)
|
||||||
const smtpDoc = await smtpResponse.json()
|
const smtpDoc = await smtpResponse.json()
|
||||||
|
|
||||||
if (!smtpDoc._id) {
|
if (!smtpDoc._id) {
|
||||||
|
@ -92,10 +94,15 @@
|
||||||
requireAuth = smtpConfig.config.auth != null
|
requireAuth = smtpConfig.config.auth != null
|
||||||
// always attach the auth for the forms purpose -
|
// always attach the auth for the forms purpose -
|
||||||
// this will be removed later if required
|
// this will be removed later if required
|
||||||
|
if (!smtpDoc.config) {
|
||||||
|
smtpDoc.config = {}
|
||||||
|
}
|
||||||
|
if (!smtpDoc.config.auth) {
|
||||||
smtpConfig.config.auth = {
|
smtpConfig.config.auth = {
|
||||||
type: "login",
|
type: "login",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fetchSmtp()
|
fetchSmtp()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -47,8 +47,8 @@
|
||||||
})
|
})
|
||||||
let selectedApp
|
let selectedApp
|
||||||
|
|
||||||
const userFetch = fetchData(`/api/admin/users/${userId}`)
|
const userFetch = fetchData(`/api/global/users/${userId}`)
|
||||||
const apps = fetchData(`/api/admin/roles`)
|
const apps = fetchData(`/api/global/roles`)
|
||||||
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
const res = await users.delete(userId)
|
const res = await users.delete(userId)
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
async function uploadLogo(file) {
|
async function uploadLogo(file) {
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("file", file)
|
data.append("file", file)
|
||||||
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
|
const res = await post("/api/global/configs/upload/settings/logo", data, {})
|
||||||
return await res.json()
|
return await res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$redirect("./builder")
|
import { auth } from "../stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
auth.checkQueryString()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
$redirect(`./builder`)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
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 admin = writable({
|
||||||
|
loaded: false,
|
||||||
|
})
|
||||||
|
|
||||||
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/global/configs/checklist?tenantId=${tenantId}`
|
||||||
|
)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
const onboardingSteps = Object.keys(json)
|
const onboardingSteps = Object.keys(json)
|
||||||
|
@ -16,20 +22,49 @@ export function createAdminStore() {
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
set({
|
await multiTenancyEnabled()
|
||||||
checklist: json,
|
admin.update(store => {
|
||||||
onboardingProgress: (stepsComplete / onboardingSteps.length) * 100,
|
store.loaded = true
|
||||||
|
store.checklist = json
|
||||||
|
store.onboardingProgress =
|
||||||
|
(stepsComplete / onboardingSteps.length) * 100
|
||||||
|
return store
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
set({
|
admin.update(store => {
|
||||||
checklist: null,
|
store.checklist = null
|
||||||
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function multiTenancyEnabled() {
|
||||||
|
let enabled = false
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/system/flags`)
|
||||||
|
const json = await response.json()
|
||||||
|
enabled = json.multiTenancy
|
||||||
|
} catch (err) {
|
||||||
|
// just let it stay disabled
|
||||||
|
}
|
||||||
|
admin.update(store => {
|
||||||
|
store.multiTenancy = enabled
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
admin.update(store => {
|
||||||
|
store.loaded = false
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe: admin.subscribe,
|
||||||
init,
|
init,
|
||||||
|
unload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,74 +1,124 @@
|
||||||
import { derived, writable, get } from "svelte/store"
|
import { derived, writable, get } from "svelte/store"
|
||||||
import api from "../../builderStore/api"
|
import api from "../../builderStore/api"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
export function createAuthStore() {
|
export function createAuthStore() {
|
||||||
const user = writable(null)
|
const auth = writable({
|
||||||
const store = derived(user, $user => {
|
user: null,
|
||||||
|
tenantId: "default",
|
||||||
|
tenantSet: false,
|
||||||
|
loaded: false,
|
||||||
|
})
|
||||||
|
const store = derived(auth, $store => {
|
||||||
let initials = null
|
let initials = null
|
||||||
let isAdmin = false
|
let isAdmin = false
|
||||||
let isBuilder = false
|
let isBuilder = false
|
||||||
if ($user) {
|
if ($store.user) {
|
||||||
if ($user.firstName) {
|
const user = $store.user
|
||||||
initials = $user.firstName[0]
|
if (user.firstName) {
|
||||||
if ($user.lastName) {
|
initials = user.firstName[0]
|
||||||
initials += $user.lastName[0]
|
if (user.lastName) {
|
||||||
|
initials += user.lastName[0]
|
||||||
}
|
}
|
||||||
} else if ($user.email) {
|
} else if (user.email) {
|
||||||
initials = $user.email[0]
|
initials = user.email[0]
|
||||||
} else {
|
} else {
|
||||||
initials = "Unknown"
|
initials = "Unknown"
|
||||||
}
|
}
|
||||||
isAdmin = !!$user.admin?.global
|
isAdmin = !!user.admin?.global
|
||||||
isBuilder = !!$user.builder?.global
|
isBuilder = !!user.builder?.global
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
user: $user,
|
user: $store.user,
|
||||||
|
tenantId: $store.tenantId,
|
||||||
|
tenantSet: $store.tenantSet,
|
||||||
|
loaded: $store.loaded,
|
||||||
initials,
|
initials,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isBuilder,
|
isBuilder,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function setUser(user) {
|
||||||
|
auth.update(store => {
|
||||||
|
store.loaded = true
|
||||||
|
store.user = user
|
||||||
|
if (user) {
|
||||||
|
store.tenantId = user.tenantId || "default"
|
||||||
|
store.tenantSet = true
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setOrganisation(tenantId) {
|
||||||
|
const prevId = get(store).tenantId
|
||||||
|
auth.update(store => {
|
||||||
|
store.tenantId = tenantId
|
||||||
|
store.tenantSet = !!tenantId
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
if (prevId !== tenantId) {
|
||||||
|
// re-init admin after setting org
|
||||||
|
await admin.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
|
checkQueryString: async () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
if (urlParams.has("tenantId")) {
|
||||||
|
const tenantId = urlParams.get("tenantId")
|
||||||
|
await setOrganisation(tenantId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOrg: async tenantId => {
|
||||||
|
await setOrganisation(tenantId)
|
||||||
|
},
|
||||||
checkAuth: async () => {
|
checkAuth: async () => {
|
||||||
const response = await api.get("/api/admin/users/self")
|
const response = await api.get("/api/global/users/self")
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
user.set(null)
|
setUser(null)
|
||||||
} else {
|
} else {
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
user.set(json)
|
setUser(json)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: async creds => {
|
login: async creds => {
|
||||||
const response = await api.post(`/api/admin/auth`, creds)
|
const tenantId = get(store).tenantId
|
||||||
|
const response = await api.post(
|
||||||
|
`/api/global/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)
|
setUser(json.user)
|
||||||
} else {
|
} else {
|
||||||
throw "Invalid credentials"
|
throw "Invalid credentials"
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
const response = await api.post(`/api/admin/auth/logout`)
|
const response = await api.post(`/api/global/auth/logout`)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw "Unable to create logout"
|
throw "Unable to create logout"
|
||||||
}
|
}
|
||||||
await response.json()
|
await response.json()
|
||||||
user.set(null)
|
setUser(null)
|
||||||
},
|
},
|
||||||
updateSelf: async fields => {
|
updateSelf: async fields => {
|
||||||
const newUser = { ...get(user), ...fields }
|
const newUser = { ...get(auth).user, ...fields }
|
||||||
const response = await api.post("/api/admin/users/self", newUser)
|
const response = await api.post("/api/global/users/self", newUser)
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
user.set(newUser)
|
setUser(newUser)
|
||||||
} else {
|
} else {
|
||||||
throw "Unable to update user details"
|
throw "Unable to update user details"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
forgotPassword: async email => {
|
forgotPassword: async email => {
|
||||||
const response = await api.post(`/api/admin/auth/reset`, {
|
const tenantId = get(store).tenantId
|
||||||
|
const response = await api.post(`/api/global/auth/${tenantId}/reset`, {
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
|
@ -77,17 +127,21 @@ export function createAuthStore() {
|
||||||
await response.json()
|
await response.json()
|
||||||
},
|
},
|
||||||
resetPassword: async (password, code) => {
|
resetPassword: async (password, code) => {
|
||||||
const response = await api.post(`/api/admin/auth/reset/update`, {
|
const tenantId = get(store).tenantId
|
||||||
|
const response = await api.post(
|
||||||
|
`/api/global/auth/${tenantId}/reset/update`,
|
||||||
|
{
|
||||||
password,
|
password,
|
||||||
resetCode: code,
|
resetCode: code,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw "Unable to reset password"
|
throw "Unable to reset password"
|
||||||
}
|
}
|
||||||
await response.json()
|
await response.json()
|
||||||
},
|
},
|
||||||
createUser: async user => {
|
createUser: async user => {
|
||||||
const response = await api.post(`/api/admin/users`, user)
|
const response = await api.post(`/api/global/users`, user)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw "Unable to create user"
|
throw "Unable to create user"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,11 @@ export function createEmailStore() {
|
||||||
templates: {
|
templates: {
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
// fetch the email template definitions
|
// fetch the email template definitions
|
||||||
const response = await api.get(`/api/admin/template/definitions`)
|
const response = await api.get(`/api/global/template/definitions`)
|
||||||
const definitions = await response.json()
|
const definitions = await response.json()
|
||||||
|
|
||||||
// fetch the email templates themselves
|
// fetch the email templates themselves
|
||||||
const templatesResponse = await api.get(`/api/admin/template/email`)
|
const templatesResponse = await api.get(`/api/global/template/email`)
|
||||||
const templates = await templatesResponse.json()
|
const templates = await templatesResponse.json()
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
|
@ -23,7 +23,7 @@ export function createEmailStore() {
|
||||||
},
|
},
|
||||||
save: async template => {
|
save: async template => {
|
||||||
// Save your template config
|
// Save your template config
|
||||||
const response = await api.post(`/api/admin/template`, template)
|
const response = await api.post(`/api/global/template`, template)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if (response.status !== 200) throw new Error(json.message)
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
template._rev = json._rev
|
template._rev = json._rev
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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"
|
||||||
|
|
||||||
const OIDC_CONFIG = {
|
const OIDC_CONFIG = {
|
||||||
logo: undefined,
|
logo: undefined,
|
||||||
|
@ -12,10 +13,13 @@ export function createOidcStore() {
|
||||||
const { set, subscribe } = store
|
const { set, subscribe } = store
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const res = await api.get(`/api/admin/configs/publicOidc`)
|
const tenantId = get(auth).tenantId
|
||||||
|
const res = await api.get(
|
||||||
|
`/api/global/configs/public/oidc?tenantId=${tenantId}`
|
||||||
|
)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
if (json.status === 400) {
|
if (json.status === 400 || Object.keys(json).length === 0) {
|
||||||
set(OIDC_CONFIG)
|
set(OIDC_CONFIG)
|
||||||
} else {
|
} else {
|
||||||
// Just use the first config for now. We will be support multiple logins buttons later on.
|
// Just use the first config for now. We will be support multiple logins buttons later on.
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
platformUrl: "http://localhost:1000",
|
platformUrl: "http://localhost:10000",
|
||||||
logoUrl: undefined,
|
logoUrl: undefined,
|
||||||
docsUrl: undefined,
|
docsUrl: undefined,
|
||||||
company: "Budibase",
|
company: "Budibase",
|
||||||
|
@ -15,7 +16,8 @@ export function createOrganisationStore() {
|
||||||
const { subscribe, set } = store
|
const { subscribe, set } = store
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const res = await api.get(`/api/admin/configs/public`)
|
const tenantId = get(auth).tenantId
|
||||||
|
const res = await api.get(`/api/global/configs/public?tenantId=${tenantId}`)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
if (json.status === 400) {
|
if (json.status === 400) {
|
||||||
|
@ -26,7 +28,7 @@ export function createOrganisationStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(config) {
|
async function save(config) {
|
||||||
const res = await api.post("/api/admin/configs", {
|
const res = await api.post("/api/global/configs", {
|
||||||
type: "settings",
|
type: "settings",
|
||||||
config: { ...get(store), ...config },
|
config: { ...get(store), ...config },
|
||||||
_rev: get(store)._rev,
|
_rev: get(store)._rev,
|
||||||
|
|
|
@ -6,7 +6,7 @@ export function createUsersStore() {
|
||||||
const { subscribe, set } = writable([])
|
const { subscribe, set } = writable([])
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const response = await api.get(`/api/admin/users`)
|
const response = await api.get(`/api/global/users`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
set(json)
|
set(json)
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,12 @@ export function createUsersStore() {
|
||||||
global: true,
|
global: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const response = await api.post(`/api/admin/users/invite`, body)
|
const response = await api.post(`/api/global/users/invite`, body)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password) {
|
||||||
const response = await api.post("/api/admin/users/invite/accept", {
|
const response = await api.post("/api/global/users/invite/accept", {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
@ -47,20 +47,20 @@ export function createUsersStore() {
|
||||||
if (admin) {
|
if (admin) {
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
}
|
}
|
||||||
const response = await api.post("/api/admin/users", body)
|
const response = await api.post("/api/global/users", body)
|
||||||
await init()
|
await init()
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(id) {
|
async function del(id) {
|
||||||
const response = await api.delete(`/api/admin/users/${id}`)
|
const response = await api.delete(`/api/global/users/${id}`)
|
||||||
update(users => users.filter(user => user._id !== id))
|
update(users => users.filter(user => user._id !== id))
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(data) {
|
async function save(data) {
|
||||||
try {
|
try {
|
||||||
const res = await post(`/api/admin/users`, data)
|
const res = await post(`/api/global/users`, data)
|
||||||
return await res.json()
|
return await res.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const logIn = async ({ email, password }) => {
|
||||||
return API.error("Please enter your password")
|
return API.error("Please enter your password")
|
||||||
}
|
}
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/admin/auth",
|
url: "/api/global/auth",
|
||||||
body: { username: email, password },
|
body: { username: email, password },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export const logIn = async ({ email, password }) => {
|
||||||
*/
|
*/
|
||||||
export const fetchSelf = async () => {
|
export const fetchSelf = async () => {
|
||||||
const user = await API.get({ url: "/api/self" })
|
const user = await API.get({ url: "/api/self" })
|
||||||
if (user?._id) {
|
if (user && user._id) {
|
||||||
if (user.roleId === "PUBLIC") {
|
if (user.roleId === "PUBLIC") {
|
||||||
// Don't try to enrich a public user as it will 403
|
// Don't try to enrich a public user as it will 403
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -16,7 +16,7 @@ module FetchMock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.includes("/api/admin")) {
|
if (url.includes("/api/global")) {
|
||||||
return json({
|
return json({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
_id: "us_test@test.com",
|
_id: "us_test@test.com",
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
|
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
|
||||||
"lint": "eslint --fix src/",
|
"lint": "eslint --fix src/",
|
||||||
"lint:fix": "yarn run format && yarn run lint",
|
"lint:fix": "yarn run format && yarn run lint",
|
||||||
"initialise": "node scripts/initialise.js"
|
"initialise": "node scripts/initialise.js",
|
||||||
|
"multi:enable": "node scripts/multiTenancy.js enable",
|
||||||
|
"multi:disable": "node scripts/multiTenancy.js disable"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
@ -136,7 +138,8 @@
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.3.4",
|
||||||
|
"update-dotenv": "^1.1.1"
|
||||||
},
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ async function init() {
|
||||||
fs.writeFileSync(envoyOutputPath, processStringSync(contents, config))
|
fs.writeFileSync(envoyOutputPath, processStringSync(contents, config))
|
||||||
|
|
||||||
const envFilePath = path.join(process.cwd(), ".env")
|
const envFilePath = path.join(process.cwd(), ".env")
|
||||||
|
if (!fs.existsSync(envFilePath)) {
|
||||||
const envFileJson = {
|
const envFileJson = {
|
||||||
PORT: 4001,
|
PORT: 4001,
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
|
@ -47,6 +48,7 @@ async function init() {
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
COUCH_DB_PASSWORD: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
|
MULTI_TENANCY: "",
|
||||||
}
|
}
|
||||||
let envFile = ""
|
let envFile = ""
|
||||||
Object.keys(envFileJson).forEach(key => {
|
Object.keys(envFileJson).forEach(key => {
|
||||||
|
@ -54,6 +56,7 @@ async function init() {
|
||||||
})
|
})
|
||||||
fs.writeFileSync(envFilePath, envFile)
|
fs.writeFileSync(envFilePath, envFile)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function up() {
|
async function up() {
|
||||||
console.log("Spinning up your budibase dev environment... 🔧✨")
|
console.log("Spinning up your budibase dev environment... 🔧✨")
|
||||||
|
|
|
@ -10,6 +10,7 @@ CREATE TABLE Persons (
|
||||||
CREATE TABLE Tasks (
|
CREATE TABLE Tasks (
|
||||||
TaskID SERIAL PRIMARY KEY,
|
TaskID SERIAL PRIMARY KEY,
|
||||||
PersonID INT,
|
PersonID INT,
|
||||||
|
Completed BOOLEAN,
|
||||||
TaskName varchar(255),
|
TaskName varchar(255),
|
||||||
CONSTRAINT fkPersons
|
CONSTRAINT fkPersons
|
||||||
FOREIGN KEY(PersonID)
|
FOREIGN KEY(PersonID)
|
||||||
|
@ -31,8 +32,8 @@ CREATE TABLE Products_Tasks (
|
||||||
PRIMARY KEY (ProductID, TaskID)
|
PRIMARY KEY (ProductID, TaskID)
|
||||||
);
|
);
|
||||||
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
|
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
|
||||||
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
|
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'assembling', TRUE);
|
||||||
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
|
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'processing', FALSE);
|
||||||
INSERT INTO Products (ProductName) VALUES ('Computers');
|
INSERT INTO Products (ProductName) VALUES ('Computers');
|
||||||
INSERT INTO Products (ProductName) VALUES ('Laptops');
|
INSERT INTO Products (ProductName) VALUES ('Laptops');
|
||||||
INSERT INTO Products (ProductName) VALUES ('Chairs');
|
INSERT INTO Products (ProductName) VALUES ('Chairs');
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const updateDotEnv = require("update-dotenv")
|
||||||
|
|
||||||
|
const arg = process.argv.slice(2)[0]
|
||||||
|
|
||||||
|
updateDotEnv({
|
||||||
|
MULTI_TENANCY: arg === "enable" ? "1" : "",
|
||||||
|
}).then(() => console.log("Updated server!"))
|
|
@ -1,8 +1,30 @@
|
||||||
const builderDB = require("../../db/builder")
|
const { StaticDatabases } = require("@budibase/auth/db")
|
||||||
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
|
const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys
|
||||||
|
|
||||||
|
async function getBuilderMainDoc() {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
try {
|
||||||
|
return await db.get(KEYS_DOC)
|
||||||
|
} catch (err) {
|
||||||
|
// doesn't exist yet, nothing to get
|
||||||
|
return {
|
||||||
|
_id: KEYS_DOC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setBuilderMainDoc(doc) {
|
||||||
|
// make sure to override the ID
|
||||||
|
doc._id = KEYS_DOC
|
||||||
|
const db = getGlobalDB()
|
||||||
|
return db.put(doc)
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
try {
|
try {
|
||||||
const mainDoc = await builderDB.getBuilderMainDoc()
|
const mainDoc = await getBuilderMainDoc()
|
||||||
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
|
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
|
@ -15,12 +37,12 @@ exports.update = async function (ctx) {
|
||||||
const value = ctx.request.body.value
|
const value = ctx.request.body.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mainDoc = await builderDB.getBuilderMainDoc()
|
const mainDoc = await getBuilderMainDoc()
|
||||||
if (mainDoc.apiKeys == null) {
|
if (mainDoc.apiKeys == null) {
|
||||||
mainDoc.apiKeys = {}
|
mainDoc.apiKeys = {}
|
||||||
}
|
}
|
||||||
mainDoc.apiKeys[key] = value
|
mainDoc.apiKeys[key] = value
|
||||||
const resp = await builderDB.setBuilderMainDoc(mainDoc)
|
const resp = await setBuilderMainDoc(mainDoc)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
_id: resp.id,
|
_id: resp.id,
|
||||||
_rev: resp.rev,
|
_rev: resp.rev,
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -38,6 +38,7 @@ const {
|
||||||
backupClientLibrary,
|
backupClientLibrary,
|
||||||
revertClientLibrary,
|
revertClientLibrary,
|
||||||
} = require("../../utilities/fileSystem/clientLibrary")
|
} = require("../../utilities/fileSystem/clientLibrary")
|
||||||
|
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
const URL_REGEX_SLASH = /\/|\\/g
|
const URL_REGEX_SLASH = /\/|\\/g
|
||||||
|
|
||||||
|
@ -93,7 +94,8 @@ async function getAppUrlIfNotInUse(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInstance(template) {
|
async function createInstance(template) {
|
||||||
const baseAppId = generateAppID()
|
const tenantId = isMultiTenant() ? getTenantId() : null
|
||||||
|
const baseAppId = generateAppID(tenantId)
|
||||||
const appId = generateDevAppID(baseAppId)
|
const appId = generateDevAppID(baseAppId)
|
||||||
|
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
@ -128,7 +130,7 @@ 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 apps = await getAllApps(CouchDB, { dev, all })
|
||||||
|
|
||||||
// get the locks for all the dev apps
|
// get the locks for all the dev apps
|
||||||
if (dev || all) {
|
if (dev || all) {
|
||||||
|
@ -220,10 +222,12 @@ exports.create = async function (ctx) {
|
||||||
url: url,
|
url: url,
|
||||||
template: ctx.request.body.template,
|
template: ctx.request.body.template,
|
||||||
instance: instance,
|
instance: instance,
|
||||||
|
tenantId: getTenantId(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
await db.put(newApplication, { force: true })
|
const response = await db.put(newApplication, { force: true })
|
||||||
|
newApplication._rev = response.rev
|
||||||
|
|
||||||
await createEmptyAppPackage(ctx, newApplication)
|
await createEmptyAppPackage(ctx, newApplication)
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
|
@ -295,7 +299,7 @@ exports.delete = async function (ctx) {
|
||||||
await deleteApp(ctx.params.appId)
|
await deleteApp(ctx.params.appId)
|
||||||
}
|
}
|
||||||
// make sure the app/role doesn't stick around after the app has been deleted
|
// make sure the app/role doesn't stick around after the app has been deleted
|
||||||
await removeAppFromUserRoles(ctx.params.appId)
|
await removeAppFromUserRoles(ctx, ctx.params.appId)
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
|
|
|
@ -22,7 +22,7 @@ exports.fetchSelf = async ctx => {
|
||||||
const userTable = await db.get(InternalTables.USER_METADATA)
|
const userTable = await db.get(InternalTables.USER_METADATA)
|
||||||
const metadata = await db.get(userId)
|
const metadata = await db.get(userId)
|
||||||
// specifically needs to make sure is enriched
|
// specifically needs to make sure is enriched
|
||||||
ctx.body = await outputProcessing(appId, userTable, {
|
ctx.body = await outputProcessing(ctx, userTable, {
|
||||||
...user,
|
...user,
|
||||||
...metadata,
|
...metadata,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const PouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const Deployment = require("./Deployment")
|
const Deployment = require("./Deployment")
|
||||||
const { Replication, StaticDatabases } = require("@budibase/auth/db")
|
const { Replication } = require("@budibase/auth/db")
|
||||||
const { DocumentTypes } = require("../../../db/utils")
|
const { DocumentTypes } = require("../../../db/utils")
|
||||||
|
|
||||||
// the max time we can wait for an invalidation to complete before considering it failed
|
// the max time we can wait for an invalidation to complete before considering it failed
|
||||||
|
@ -31,13 +31,14 @@ async function checkAllDeployments(deployments) {
|
||||||
async function storeDeploymentHistory(deployment) {
|
async function storeDeploymentHistory(deployment) {
|
||||||
const appId = deployment.getAppId()
|
const appId = deployment.getAppId()
|
||||||
const deploymentJSON = deployment.getJSON()
|
const deploymentJSON = deployment.getJSON()
|
||||||
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
|
const db = new CouchDB(appId)
|
||||||
|
|
||||||
let deploymentDoc
|
let deploymentDoc
|
||||||
try {
|
try {
|
||||||
deploymentDoc = await db.get(appId)
|
// theres only one deployment doc per app database
|
||||||
|
deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
deploymentDoc = { _id: appId, history: {} }
|
deploymentDoc = { _id: DocumentTypes.DEPLOYMENTS, history: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploymentId = deploymentJSON._id
|
const deploymentId = deploymentJSON._id
|
||||||
|
@ -67,7 +68,7 @@ async function deployApp(deployment) {
|
||||||
})
|
})
|
||||||
|
|
||||||
await replication.replicate()
|
await replication.replicate()
|
||||||
const db = new PouchDB(productionAppId)
|
const db = new CouchDB(productionAppId)
|
||||||
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
||||||
appDoc.appId = productionAppId
|
appDoc.appId = productionAppId
|
||||||
appDoc.instance._id = productionAppId
|
appDoc.instance._id = productionAppId
|
||||||
|
@ -98,8 +99,9 @@ async function deployApp(deployment) {
|
||||||
|
|
||||||
exports.fetchDeployments = async function (ctx) {
|
exports.fetchDeployments = async function (ctx) {
|
||||||
try {
|
try {
|
||||||
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
|
const appId = ctx.appId
|
||||||
const deploymentDoc = await db.get(ctx.appId)
|
const db = new CouchDB(appId)
|
||||||
|
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
|
||||||
const { updated, deployments } = await checkAllDeployments(
|
const { updated, deployments } = await checkAllDeployments(
|
||||||
deploymentDoc,
|
deploymentDoc,
|
||||||
ctx.user
|
ctx.user
|
||||||
|
@ -115,8 +117,9 @@ exports.fetchDeployments = async function (ctx) {
|
||||||
|
|
||||||
exports.deploymentProgress = async function (ctx) {
|
exports.deploymentProgress = async function (ctx) {
|
||||||
try {
|
try {
|
||||||
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
|
const appId = ctx.appId
|
||||||
const deploymentDoc = await db.get(ctx.appId)
|
const db = new CouchDB(appId)
|
||||||
|
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
|
||||||
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(
|
ctx.throw(
|
||||||
|
|
|
@ -9,8 +9,9 @@ const { DocumentTypes } = require("../../db/utils")
|
||||||
|
|
||||||
async function redirect(ctx, method) {
|
async function redirect(ctx, method) {
|
||||||
const { devPath } = ctx.params
|
const { devPath } = ctx.params
|
||||||
|
const queryString = ctx.originalUrl.split("?")[1] || ""
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${devPath}`),
|
checkSlashesInUrl(`${env.WORKER_URL}/api/global/${devPath}?${queryString}`),
|
||||||
request(
|
request(
|
||||||
ctx,
|
ctx,
|
||||||
{
|
{
|
||||||
|
|
|
@ -161,7 +161,7 @@ exports.fetchView = async ctx => {
|
||||||
schema: {},
|
schema: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows = await outputProcessing(appId, table, response.rows)
|
rows = await outputProcessing(ctx, table, response.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculation === CALCULATION_TYPES.STATS) {
|
if (calculation === CALCULATION_TYPES.STATS) {
|
||||||
|
@ -204,7 +204,7 @@ exports.fetch = async ctx => {
|
||||||
)
|
)
|
||||||
rows = response.rows.map(row => row.doc)
|
rows = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
return outputProcessing(appId, table, rows)
|
return outputProcessing(ctx, table, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async ctx => {
|
exports.find = async ctx => {
|
||||||
|
@ -212,7 +212,7 @@ exports.find = async ctx => {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const table = await db.get(ctx.params.tableId)
|
const table = await db.get(ctx.params.tableId)
|
||||||
let row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
|
let row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
|
||||||
row = await outputProcessing(appId, table, row)
|
row = await outputProcessing(ctx, table, row)
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,7 +291,7 @@ exports.search = async ctx => {
|
||||||
// Enrich search results with relationships
|
// Enrich search results with relationships
|
||||||
if (response.rows && response.rows.length) {
|
if (response.rows && response.rows.length) {
|
||||||
const table = await db.get(tableId)
|
const table = await db.get(tableId)
|
||||||
response.rows = await outputProcessing(appId, table, response.rows)
|
response.rows = await outputProcessing(ctx, table, response.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -328,7 +328,7 @@ exports.fetchEnrichedRow = async ctx => {
|
||||||
})
|
})
|
||||||
// need to include the IDs in these rows for any links they may have
|
// need to include the IDs in these rows for any links they may have
|
||||||
let linkedRows = await outputProcessing(
|
let linkedRows = await outputProcessing(
|
||||||
appId,
|
ctx,
|
||||||
table,
|
table,
|
||||||
response.rows.map(row => row.doc)
|
response.rows.map(row => row.doc)
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,7 +17,7 @@ function removeGlobalProps(user) {
|
||||||
|
|
||||||
exports.fetchMetadata = async function (ctx) {
|
exports.fetchMetadata = async function (ctx) {
|
||||||
const database = new CouchDB(ctx.appId)
|
const database = new CouchDB(ctx.appId)
|
||||||
const global = await getGlobalUsers(ctx.appId)
|
const global = await getGlobalUsers(ctx, ctx.appId)
|
||||||
const metadata = (
|
const metadata = (
|
||||||
await database.allDocs(
|
await database.allDocs(
|
||||||
getUserMetadataParams(null, {
|
getUserMetadataParams(null, {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
|
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } =
|
||||||
|
require("@budibase/auth").auth
|
||||||
const currentApp = require("../middleware/currentapp")
|
const currentApp = require("../middleware/currentapp")
|
||||||
const compress = require("koa-compress")
|
const compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
|
@ -9,6 +10,13 @@ const env = require("../environment")
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
|
const NO_TENANCY_ENDPOINTS = [
|
||||||
|
{
|
||||||
|
route: "/api/analytics",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
compress({
|
compress({
|
||||||
|
@ -36,6 +44,8 @@ router
|
||||||
publicAllowed: true,
|
publicAllowed: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
// nothing in the server should allow query string tenants
|
||||||
|
.use(buildTenancyMiddleware(null, NO_TENANCY_ENDPOINTS))
|
||||||
.use(currentApp)
|
.use(currentApp)
|
||||||
.use(auditLog)
|
.use(auditLog)
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ const { BUILDER } = require("@budibase/auth/permissions")
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
|
.post("/api/applications", authorized(BUILDER), controller.create)
|
||||||
.get("/api/applications/:appId/definition", controller.fetchAppDefinition)
|
.get("/api/applications/:appId/definition", controller.fetchAppDefinition)
|
||||||
.get("/api/applications", controller.fetch)
|
.get("/api/applications", controller.fetch)
|
||||||
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
|
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
|
||||||
.put("/api/applications/:appId", authorized(BUILDER), controller.update)
|
.put("/api/applications/:appId", authorized(BUILDER), controller.update)
|
||||||
.post("/api/applications", authorized(BUILDER), controller.create)
|
|
||||||
.post(
|
.post(
|
||||||
"/api/applications/:appId/client/update",
|
"/api/applications/:appId/client/update",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
|
|
|
@ -8,9 +8,9 @@ const router = Router()
|
||||||
|
|
||||||
if (env.isDev() || env.isTest()) {
|
if (env.isDev() || env.isTest()) {
|
||||||
router
|
router
|
||||||
.get("/api/admin/:devPath(.*)", controller.redirectGet)
|
.get("/api/global/:devPath(.*)", controller.redirectGet)
|
||||||
.post("/api/admin/:devPath(.*)", controller.redirectPost)
|
.post("/api/global/:devPath(.*)", controller.redirectPost)
|
||||||
.delete("/api/admin/:devPath(.*)", controller.redirectDelete)
|
.delete("/api/global/:devPath(.*)", controller.redirectDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
|
|
|
@ -387,7 +387,7 @@ describe("/rows", () => {
|
||||||
})
|
})
|
||||||
// the environment needs configured for this
|
// the environment needs configured for this
|
||||||
await setup.switchToSelfHosted(async () => {
|
await setup.switchToSelfHosted(async () => {
|
||||||
const enriched = await outputProcessing(config.getAppId(), table, [row])
|
const enriched = await outputProcessing({ appId: config.getAppId() }, table, [row])
|
||||||
expect(enriched[0].attachment[0].url).toBe(
|
expect(enriched[0].attachment[0].url).toBe(
|
||||||
`/prod-budi-app-assets/${config.getAppId()}/attachments/test/thing.csv`
|
`/prod-budi-app-assets/${config.getAppId()}/attachments/test/thing.csv`
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@ const appController = require("../../../controllers/application")
|
||||||
const CouchDB = require("../../../../db")
|
const CouchDB = require("../../../../db")
|
||||||
const { AppStatus } = require("../../../../db/utils")
|
const { AppStatus } = require("../../../../db/utils")
|
||||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||||
|
const { TENANT_ID } = require("../../../../tests/utilities/structures")
|
||||||
|
|
||||||
function Request(appId, params) {
|
function Request(appId, params) {
|
||||||
this.appId = appId
|
this.appId = appId
|
||||||
|
@ -16,8 +17,8 @@ exports.getAllTableRows = async config => {
|
||||||
return req.body
|
return req.body
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.clearAllApps = async () => {
|
exports.clearAllApps = async (tenantId = TENANT_ID) => {
|
||||||
const req = { query: { status: AppStatus.DEV } }
|
const req = { query: { status: AppStatus.DEV }, user: { tenantId } }
|
||||||
await appController.fetch(req)
|
await appController.fetch(req)
|
||||||
const apps = req.body
|
const apps = req.body
|
||||||
if (!apps || apps.length <= 0) {
|
if (!apps || apps.length <= 0) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ module.exports.definition = {
|
||||||
properties: {
|
properties: {
|
||||||
text: {
|
text: {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "URL",
|
title: "Log",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["text"],
|
required: ["text"],
|
||||||
|
|
|
@ -3,6 +3,10 @@ const logic = require("./logic")
|
||||||
const automationUtils = require("./automationUtils")
|
const automationUtils = require("./automationUtils")
|
||||||
const AutomationEmitter = require("../events/AutomationEmitter")
|
const AutomationEmitter = require("../events/AutomationEmitter")
|
||||||
const { processObject } = require("@budibase/string-templates")
|
const { processObject } = require("@budibase/string-templates")
|
||||||
|
const { DEFAULT_TENANT_ID } = require("@budibase/auth").constants
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
const { DocumentTypes } = require("../db/utils")
|
||||||
|
const { doInTenant } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||||
|
|
||||||
|
@ -16,6 +20,7 @@ class Orchestrator {
|
||||||
this._metadata = triggerOutput.metadata
|
this._metadata = triggerOutput.metadata
|
||||||
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
|
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
|
||||||
this._appId = triggerOutput.appId
|
this._appId = triggerOutput.appId
|
||||||
|
this._app = null
|
||||||
// remove from context
|
// remove from context
|
||||||
delete triggerOutput.appId
|
delete triggerOutput.appId
|
||||||
delete triggerOutput.metadata
|
delete triggerOutput.metadata
|
||||||
|
@ -40,8 +45,19 @@ class Orchestrator {
|
||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getApp() {
|
||||||
|
const appId = this._appId
|
||||||
|
if (this._app) {
|
||||||
|
return this._app
|
||||||
|
}
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
this._app = await db.get(DocumentTypes.APP_METADATA)
|
||||||
|
return this._app
|
||||||
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
let automation = this._automation
|
let automation = this._automation
|
||||||
|
const app = await this.getApp()
|
||||||
for (let step of automation.definition.steps) {
|
for (let step of automation.definition.steps) {
|
||||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||||
step.inputs = await processObject(step.inputs, this._context)
|
step.inputs = await processObject(step.inputs, this._context)
|
||||||
|
@ -51,13 +67,16 @@ class Orchestrator {
|
||||||
)
|
)
|
||||||
// appId is always passed
|
// appId is always passed
|
||||||
try {
|
try {
|
||||||
const outputs = await stepFn({
|
let tenantId = app.tenantId || DEFAULT_TENANT_ID
|
||||||
|
const outputs = await doInTenant(tenantId, () => {
|
||||||
|
return stepFn({
|
||||||
inputs: step.inputs,
|
inputs: step.inputs,
|
||||||
appId: this._appId,
|
appId: this._appId,
|
||||||
apiKey: automation.apiKey,
|
apiKey: automation.apiKey,
|
||||||
emitter: this._emitter,
|
emitter: this._emitter,
|
||||||
context: this._context,
|
context: this._context,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
const CouchDB = require("./index")
|
|
||||||
const { StaticDatabases } = require("./utils")
|
|
||||||
const env = require("../environment")
|
|
||||||
|
|
||||||
const SELF_HOST_ERR = "Unable to access builder DB/doc - not self hosted."
|
|
||||||
const BUILDER_DB = StaticDatabases.BUILDER
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the builder database, right now this is a single, static database
|
|
||||||
* that is present across the whole system and determines some core functionality
|
|
||||||
* for the builder (e.g. storage of API keys). This has been limited to self hosting
|
|
||||||
* as it doesn't make as much sense against the currently design Cloud system.
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.getBuilderMainDoc = async () => {
|
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
throw SELF_HOST_ERR
|
|
||||||
}
|
|
||||||
const db = new CouchDB(BUILDER_DB.name)
|
|
||||||
try {
|
|
||||||
return await db.get(BUILDER_DB.baseDoc)
|
|
||||||
} catch (err) {
|
|
||||||
// doesn't exist yet, nothing to get
|
|
||||||
return {
|
|
||||||
_id: BUILDER_DB.baseDoc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setBuilderMainDoc = async doc => {
|
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
throw SELF_HOST_ERR
|
|
||||||
}
|
|
||||||
// make sure to override the ID
|
|
||||||
doc._id = BUILDER_DB.baseDoc
|
|
||||||
const db = new CouchDB(BUILDER_DB.name)
|
|
||||||
return db.put(doc)
|
|
||||||
}
|
|
|
@ -60,7 +60,7 @@ async function getLinksForRows(appId, rows) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFullLinkedDocs(appId, links) {
|
async function getFullLinkedDocs(ctx, appId, links) {
|
||||||
// create DBs
|
// create DBs
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const linkedRowIds = links.map(link => link.id)
|
const linkedRowIds = links.map(link => link.id)
|
||||||
|
@ -71,7 +71,7 @@ async function getFullLinkedDocs(appId, links) {
|
||||||
let [users, other] = partition(linked, linkRow =>
|
let [users, other] = partition(linked, linkRow =>
|
||||||
linkRow._id.startsWith(USER_METDATA_PREFIX)
|
linkRow._id.startsWith(USER_METDATA_PREFIX)
|
||||||
)
|
)
|
||||||
const globalUsers = await getGlobalUsers(appId, users)
|
const globalUsers = await getGlobalUsers(ctx, appId, users)
|
||||||
users = users.map(user => {
|
users = users.map(user => {
|
||||||
const globalUser = globalUsers.find(
|
const globalUser = globalUsers.find(
|
||||||
globalUser => globalUser && user._id.includes(globalUser._id)
|
globalUser => globalUser && user._id.includes(globalUser._id)
|
||||||
|
@ -166,12 +166,13 @@ exports.attachLinkIDs = async (appId, rows) => {
|
||||||
/**
|
/**
|
||||||
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
||||||
* This is required for formula fields, this may only be utilised internally (for now).
|
* This is required for formula fields, this may only be utilised internally (for now).
|
||||||
* @param {string} appId The app in which the tables/rows/links exist.
|
* @param {object} ctx The request which is looking for rows.
|
||||||
* @param {object} table The table from which the rows originated.
|
* @param {object} table The table from which the rows originated.
|
||||||
* @param {array<object>} rows The rows which are to be enriched.
|
* @param {array<object>} rows The rows which are to be enriched.
|
||||||
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
|
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
|
||||||
*/
|
*/
|
||||||
exports.attachFullLinkedDocs = async (appId, table, rows) => {
|
exports.attachFullLinkedDocs = async (ctx, table, rows) => {
|
||||||
|
const appId = ctx.appId
|
||||||
const linkedTableIds = getLinkedTableIDs(table)
|
const linkedTableIds = getLinkedTableIDs(table)
|
||||||
if (linkedTableIds.length === 0) {
|
if (linkedTableIds.length === 0) {
|
||||||
return rows
|
return rows
|
||||||
|
@ -182,7 +183,7 @@ exports.attachFullLinkedDocs = async (appId, table, rows) => {
|
||||||
const links = (await getLinksForRows(appId, rows)).filter(link =>
|
const links = (await getLinksForRows(appId, rows)).filter(link =>
|
||||||
rows.some(row => row._id === link.thisId)
|
rows.some(row => row._id === link.thisId)
|
||||||
)
|
)
|
||||||
let linked = await getFullLinkedDocs(appId, links)
|
let linked = await getFullLinkedDocs(ctx, appId, links)
|
||||||
const linkedTables = []
|
const linkedTables = []
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
for (let link of links.filter(link => link.thisId === row._id)) {
|
for (let link of links.filter(link => link.thisId === row._id)) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ const DocumentTypes = {
|
||||||
DATASOURCE: "datasource",
|
DATASOURCE: "datasource",
|
||||||
DATASOURCE_PLUS: "datasource_plus",
|
DATASOURCE_PLUS: "datasource_plus",
|
||||||
QUERY: "query",
|
QUERY: "query",
|
||||||
|
DEPLOYMENTS: "deployments",
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewNames = {
|
const ViewNames = {
|
||||||
|
@ -49,13 +50,7 @@ const SearchIndexes = {
|
||||||
ROWS: "rows",
|
ROWS: "rows",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = {
|
exports.StaticDatabases = StaticDatabases
|
||||||
BUILDER: {
|
|
||||||
name: "builder-db",
|
|
||||||
baseDoc: "builder-doc",
|
|
||||||
},
|
|
||||||
...StaticDatabases,
|
|
||||||
}
|
|
||||||
|
|
||||||
const BudibaseInternalDB = {
|
const BudibaseInternalDB = {
|
||||||
_id: "bb_internal",
|
_id: "bb_internal",
|
||||||
|
@ -230,8 +225,12 @@ exports.getLinkParams = (otherProps = {}) => {
|
||||||
* Generates a new app ID.
|
* Generates a new app ID.
|
||||||
* @returns {string} The new app ID which the app doc can be stored under.
|
* @returns {string} The new app ID which the app doc can be stored under.
|
||||||
*/
|
*/
|
||||||
exports.generateAppID = () => {
|
exports.generateAppID = (tenantId = null) => {
|
||||||
return `${DocumentTypes.APP}${SEPARATOR}${newid()}`
|
let id = `${DocumentTypes.APP}${SEPARATOR}`
|
||||||
|
if (tenantId) {
|
||||||
|
id += `${tenantId}${SEPARATOR}`
|
||||||
|
}
|
||||||
|
return `${id}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,8 +239,8 @@ exports.generateAppID = () => {
|
||||||
*/
|
*/
|
||||||
exports.generateDevAppID = appId => {
|
exports.generateDevAppID = appId => {
|
||||||
const prefix = `${DocumentTypes.APP}${SEPARATOR}`
|
const prefix = `${DocumentTypes.APP}${SEPARATOR}`
|
||||||
const uuid = appId.split(prefix)[1]
|
const rest = appId.split(prefix)[1]
|
||||||
return `${DocumentTypes.APP_DEV}${SEPARATOR}${uuid}`
|
return `${DocumentTypes.APP_DEV}${SEPARATOR}${rest}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -35,6 +35,7 @@ module.exports = {
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
// environment
|
// environment
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||||
|
|
|
@ -68,5 +68,6 @@ module.exports = async (ctx, next) => {
|
||||||
) {
|
) {
|
||||||
setCookie(ctx, { appId }, Cookies.CurrentApp)
|
setCookie(ctx, { appId }, Cookies.CurrentApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,19 @@ const {
|
||||||
basicScreen,
|
basicScreen,
|
||||||
basicLayout,
|
basicLayout,
|
||||||
basicWebhook,
|
basicWebhook,
|
||||||
|
TENANT_ID,
|
||||||
} = require("./structures")
|
} = require("./structures")
|
||||||
const controllers = require("./controllers")
|
const controllers = require("./controllers")
|
||||||
const supertest = require("supertest")
|
const supertest = require("supertest")
|
||||||
const { cleanup } = require("../../utilities/fileSystem")
|
const { cleanup } = require("../../utilities/fileSystem")
|
||||||
const { Cookies, Headers } = require("@budibase/auth").constants
|
const { Cookies, Headers } = require("@budibase/auth").constants
|
||||||
const { jwt } = require("@budibase/auth").auth
|
const { jwt } = require("@budibase/auth").auth
|
||||||
const { StaticDatabases } = require("@budibase/auth/db")
|
const auth = require("@budibase/auth")
|
||||||
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
const { createASession } = require("@budibase/auth/sessions")
|
const { createASession } = require("@budibase/auth/sessions")
|
||||||
const { user: userCache } = require("@budibase/auth/cache")
|
const { user: userCache } = require("@budibase/auth/cache")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
|
auth.init(CouchDB)
|
||||||
|
|
||||||
const GLOBAL_USER_ID = "us_uuid1"
|
const GLOBAL_USER_ID = "us_uuid1"
|
||||||
const EMAIL = "babs@babs.com"
|
const EMAIL = "babs@babs.com"
|
||||||
|
@ -52,7 +55,7 @@ class TestConfiguration {
|
||||||
request.cookies = { set: () => {}, get: () => {} }
|
request.cookies = { set: () => {}, get: () => {} }
|
||||||
request.config = { jwtSecret: env.JWT_SECRET }
|
request.config = { jwtSecret: env.JWT_SECRET }
|
||||||
request.appId = this.appId
|
request.appId = this.appId
|
||||||
request.user = { appId: this.appId }
|
request.user = { appId: this.appId, tenantId: TENANT_ID }
|
||||||
request.query = {}
|
request.query = {}
|
||||||
request.request = {
|
request.request = {
|
||||||
body: config,
|
body: config,
|
||||||
|
@ -65,7 +68,7 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
|
async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
|
||||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
const db = getGlobalDB(TENANT_ID)
|
||||||
let existing
|
let existing
|
||||||
try {
|
try {
|
||||||
existing = await db.get(id)
|
existing = await db.get(id)
|
||||||
|
@ -76,8 +79,9 @@ class TestConfiguration {
|
||||||
_id: id,
|
_id: id,
|
||||||
...existing,
|
...existing,
|
||||||
roles: roles || {},
|
roles: roles || {},
|
||||||
|
tenantId: TENANT_ID,
|
||||||
}
|
}
|
||||||
await createASession(id, "sessionid")
|
await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID })
|
||||||
if (builder) {
|
if (builder) {
|
||||||
user.builder = { global: true }
|
user.builder = { global: true }
|
||||||
}
|
}
|
||||||
|
@ -107,6 +111,7 @@ class TestConfiguration {
|
||||||
const auth = {
|
const auth = {
|
||||||
userId: GLOBAL_USER_ID,
|
userId: GLOBAL_USER_ID,
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
|
tenantId: TENANT_ID,
|
||||||
}
|
}
|
||||||
const app = {
|
const app = {
|
||||||
roleId: BUILTIN_ROLE_IDS.ADMIN,
|
roleId: BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
@ -333,11 +338,15 @@ class TestConfiguration {
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
await this.createUser()
|
await this.createUser()
|
||||||
}
|
}
|
||||||
await createASession(userId, "sessionid")
|
await createASession(userId, {
|
||||||
|
sessionId: "sessionid",
|
||||||
|
tenantId: TENANT_ID,
|
||||||
|
})
|
||||||
// have to fake this
|
// have to fake this
|
||||||
const auth = {
|
const auth = {
|
||||||
userId,
|
userId,
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
|
tenantId: TENANT_ID,
|
||||||
}
|
}
|
||||||
const app = {
|
const app = {
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
|
|
|
@ -4,6 +4,8 @@ const { createHomeScreen } = require("../../constants/screens")
|
||||||
const { EMPTY_LAYOUT } = require("../../constants/layouts")
|
const { EMPTY_LAYOUT } = require("../../constants/layouts")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
|
exports.TENANT_ID = "default"
|
||||||
|
|
||||||
exports.basicTable = () => {
|
exports.basicTable = () => {
|
||||||
return {
|
return {
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
const CouchDB = require("../db")
|
|
||||||
const {
|
const {
|
||||||
getMultiIDParams,
|
getMultiIDParams,
|
||||||
getGlobalIDFromUserMetadataID,
|
getGlobalIDFromUserMetadataID,
|
||||||
StaticDatabases,
|
|
||||||
} = require("../db/utils")
|
} = require("../db/utils")
|
||||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||||
const { getGlobalUserParams } = require("@budibase/auth/db")
|
const { getGlobalUserParams } = require("@budibase/auth/db")
|
||||||
const { user: userCache } = require("@budibase/auth/cache")
|
const { user: userCache } = require("@budibase/auth/cache")
|
||||||
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
exports.updateAppRole = (appId, user) => {
|
exports.updateAppRole = (appId, user) => {
|
||||||
if (!user.roles) {
|
if (!user.roles) {
|
||||||
|
@ -34,18 +33,20 @@ function processUser(appId, user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCachedSelf = async (ctx, appId) => {
|
exports.getCachedSelf = async (ctx, appId) => {
|
||||||
|
// this has to be tenant aware, can't depend on the context to find it out
|
||||||
|
// running some middlewares before the tenancy causes context to break
|
||||||
const user = await userCache.getUser(ctx.user._id)
|
const user = await userCache.getUser(ctx.user._id)
|
||||||
return processUser(appId, user)
|
return processUser(appId, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalUser = async (appId, userId) => {
|
exports.getGlobalUser = async (ctx, appId, userId) => {
|
||||||
const db = CouchDB(StaticDatabases.GLOBAL.name)
|
const db = getGlobalDB()
|
||||||
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
|
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
|
||||||
return processUser(appId, user)
|
return processUser(appId, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalUsers = async (appId = null, users = null) => {
|
exports.getGlobalUsers = async (ctx, appId = null, users = null) => {
|
||||||
const db = CouchDB(StaticDatabases.GLOBAL.name)
|
const db = getGlobalDB()
|
||||||
let globalUsers
|
let globalUsers
|
||||||
if (users) {
|
if (users) {
|
||||||
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id))
|
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -7,8 +7,10 @@ let devAppClient, debounceClient
|
||||||
// we init this as we want to keep the connection open all the time
|
// we init this as we want to keep the connection open all the time
|
||||||
// reduces the performance hit
|
// reduces the performance hit
|
||||||
exports.init = async () => {
|
exports.init = async () => {
|
||||||
devAppClient = await new Client(utils.Databases.DEV_LOCKS).init()
|
devAppClient = new Client(utils.Databases.DEV_LOCKS)
|
||||||
debounceClient = await new Client(utils.Databases.DEBOUNCE).init()
|
debounceClient = new Client(utils.Databases.DEBOUNCE)
|
||||||
|
await devAppClient.init()
|
||||||
|
await debounceClient.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.shutdown = async () => {
|
exports.shutdown = async () => {
|
||||||
|
|
|
@ -193,20 +193,21 @@ exports.inputProcessing = (user = {}, table, row) => {
|
||||||
/**
|
/**
|
||||||
* This function enriches the input rows with anything they are supposed to contain, for example
|
* This function enriches the input rows with anything they are supposed to contain, for example
|
||||||
* link records or attachment links.
|
* link records or attachment links.
|
||||||
* @param {string} appId the ID of the application for which rows are being enriched.
|
* @param {object} ctx the request which is looking for enriched rows.
|
||||||
* @param {object} table the table from which these rows came from originally, this is used to determine
|
* @param {object} table the table from which these rows came from originally, this is used to determine
|
||||||
* the schema of the rows and then enrich.
|
* the schema of the rows and then enrich.
|
||||||
* @param {object[]} rows the rows which are to be enriched.
|
* @param {object[]} rows the rows which are to be enriched.
|
||||||
* @returns {object[]} the enriched rows will be returned.
|
* @returns {object[]} the enriched rows will be returned.
|
||||||
*/
|
*/
|
||||||
exports.outputProcessing = async (appId, table, rows) => {
|
exports.outputProcessing = async (ctx, table, rows) => {
|
||||||
|
const appId = ctx.appId
|
||||||
let wasArray = true
|
let wasArray = true
|
||||||
if (!(rows instanceof Array)) {
|
if (!(rows instanceof Array)) {
|
||||||
rows = [rows]
|
rows = [rows]
|
||||||
wasArray = false
|
wasArray = false
|
||||||
}
|
}
|
||||||
// attach any linked row information
|
// attach any linked row information
|
||||||
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
|
let enriched = await linkRows.attachFullLinkedDocs(ctx, table, rows)
|
||||||
|
|
||||||
// process formulas
|
// process formulas
|
||||||
enriched = processFormulas(table, enriched)
|
enriched = processFormulas(table, enriched)
|
||||||
|
|
|
@ -3,7 +3,7 @@ const { InternalTables } = require("../db/utils")
|
||||||
const { getGlobalUser } = require("../utilities/global")
|
const { getGlobalUser } = require("../utilities/global")
|
||||||
|
|
||||||
exports.getFullUser = async (ctx, userId) => {
|
exports.getFullUser = async (ctx, userId) => {
|
||||||
const global = await getGlobalUser(ctx.appId, userId)
|
const global = await getGlobalUser(ctx, ctx.appId, userId)
|
||||||
let metadata
|
let metadata
|
||||||
try {
|
try {
|
||||||
// this will throw an error if the db doesn't exist, or there is no appId
|
// this will throw an error if the db doesn't exist, or there is no appId
|
||||||
|
|
|
@ -4,13 +4,17 @@ const { checkSlashesInUrl } = require("./index")
|
||||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||||
const { updateAppRole, getGlobalUser } = require("./global")
|
const { updateAppRole, getGlobalUser } = require("./global")
|
||||||
const { Headers } = require("@budibase/auth/constants")
|
const { Headers } = require("@budibase/auth/constants")
|
||||||
|
const { getTenantId, isTenantIdSet } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
function request(ctx, request, noApiKey) {
|
function request(ctx, request) {
|
||||||
if (!request.headers) {
|
if (!request.headers) {
|
||||||
request.headers = {}
|
request.headers = {}
|
||||||
}
|
}
|
||||||
if (!noApiKey) {
|
if (!ctx) {
|
||||||
request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY
|
request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY
|
||||||
|
if (isTenantIdSet()) {
|
||||||
|
request.headers[Headers.TENANT_ID] = getTenantId()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (request.body && Object.keys(request.body).length > 0) {
|
if (request.body && Object.keys(request.body).length > 0) {
|
||||||
request.headers["Content-Type"] = "application/json"
|
request.headers["Content-Type"] = "application/json"
|
||||||
|
@ -29,9 +33,11 @@ function request(ctx, request, noApiKey) {
|
||||||
|
|
||||||
exports.request = request
|
exports.request = request
|
||||||
|
|
||||||
|
// have to pass in the tenant ID as this could be coming from an automation
|
||||||
exports.sendSmtpEmail = async (to, from, subject, contents) => {
|
exports.sendSmtpEmail = async (to, from, subject, contents) => {
|
||||||
|
// tenant ID will be set in header
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
|
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
|
||||||
request(null, {
|
request(null, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
|
@ -74,11 +80,11 @@ exports.getDeployedApps = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalSelf = async (ctx, appId = null) => {
|
exports.getGlobalSelf = async (ctx, appId = null) => {
|
||||||
const endpoint = `/api/admin/users/self`
|
const endpoint = `/api/global/users/self`
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
||||||
// we don't want to use API key when getting self
|
// we don't want to use API key when getting self
|
||||||
request(ctx, { method: "GET" }, true)
|
request(ctx, { method: "GET" })
|
||||||
)
|
)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
ctx.throw(400, "Unable to get self globally.")
|
ctx.throw(400, "Unable to get self globally.")
|
||||||
|
@ -96,11 +102,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
|
||||||
body = {}
|
body = {}
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
user = await exports.getGlobalSelf(ctx)
|
user = await exports.getGlobalSelf(ctx)
|
||||||
endpoint = `/api/admin/users/self`
|
endpoint = `/api/global/users/self`
|
||||||
} else {
|
} else {
|
||||||
user = await getGlobalUser(appId, userId)
|
user = await getGlobalUser(ctx, appId, userId)
|
||||||
body._id = userId
|
body._id = userId
|
||||||
endpoint = `/api/admin/users`
|
endpoint = `/api/global/users`
|
||||||
}
|
}
|
||||||
body = {
|
body = {
|
||||||
...body,
|
...body,
|
||||||
|
@ -122,11 +128,11 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.removeAppFromUserRoles = async appId => {
|
exports.removeAppFromUserRoles = async (ctx, appId) => {
|
||||||
const deployedAppId = getDeployedAppID(appId)
|
const deployedAppId = getDeployedAppID(appId)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`),
|
checkSlashesInUrl(env.WORKER_URL + `/api/global/roles/${deployedAppId}`),
|
||||||
request(null, {
|
request(ctx, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1146,12 +1146,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/auth@^0.9.80-alpha.7":
|
"@budibase/auth@^0.9.79-alpha.4":
|
||||||
version "0.9.80-alpha.7"
|
version "0.9.79"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.80-alpha.7.tgz#6fb4c40a5f437bb9f7e49c9acafbc601b0dffa49"
|
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.79.tgz#416271ffc55e84116550469656bf151a7734a90f"
|
||||||
integrity sha512-9KZy8hqdpaWRY2n3pRAThP4Jb9TsrfJsJFdfDndJtPO1tTNKtDw2LGEwrT5Kym0a0SBHEzVrXq1Vw/sg72ACIQ==
|
integrity sha512-ENh099tYeUfVExsAeoxwMh2ODioKQGPteK9LJiU5hMdM4Oi7pyImu287BgKpTIheB+WtadT4e21VpPaJ62APEw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@techpass/passport-openidconnect" "^0.3.0"
|
|
||||||
aws-sdk "^2.901.0"
|
aws-sdk "^2.901.0"
|
||||||
bcryptjs "^2.4.3"
|
bcryptjs "^2.4.3"
|
||||||
ioredis "^4.27.1"
|
ioredis "^4.27.1"
|
||||||
|
@ -1168,10 +1167,10 @@
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
zlib "^1.0.5"
|
zlib "^1.0.5"
|
||||||
|
|
||||||
"@budibase/bbui@^0.9.80-alpha.7":
|
"@budibase/bbui@^0.9.79":
|
||||||
version "0.9.80-alpha.7"
|
version "0.9.79"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.80-alpha.7.tgz#5fbb7a6617a35a560151377fdc67a845f5620803"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.79.tgz#c033ba0af41cb584d2657a8353f9887328f6633f"
|
||||||
integrity sha512-VJPP6A3BhxsLQzEfKPz3alCiT0nMqeM75P/reT1jsRxZsOCJ8vFn7g2c8aH2bEIcCqOWeUaaxVDuj8ghbzByUw==
|
integrity sha512-XxUJSPGd2FZDFdbNOeMUXohhID5h3DVq9XyKTe6WhYax4m2da/2WTENJ16UFvmfA+yxLN1qSDeweq9vw2zCahQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||||
"@spectrum-css/actionbutton" "^1.0.1"
|
"@spectrum-css/actionbutton" "^1.0.1"
|
||||||
|
@ -1216,14 +1215,14 @@
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
svelte-portal "^1.0.0"
|
svelte-portal "^1.0.0"
|
||||||
|
|
||||||
"@budibase/client@^0.9.80-alpha.7":
|
"@budibase/client@^0.9.79-alpha.4":
|
||||||
version "0.9.80-alpha.7"
|
version "0.9.79"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.80-alpha.7.tgz#9d2e98b90cd9fdcfc659826d19b5dc206cdcfe7d"
|
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.79.tgz#d1c8d51e9121f81902cfb31d3b685c8061f272a2"
|
||||||
integrity sha512-szLz2JpWI9ZMyVz7IPap1fQ7e+uphuthOkOsERplmq4EXbv914/YILdEfUm01s4aeOEOdkeogz31t8t75es6Dg==
|
integrity sha512-//Yqm5Qki6BmBe5W2Tz8GONdkFjdD1jkIU7pcLYKqdZJWEQIrX6T/xNvYvZVhw7Dx5bwSZRjFwzm7jLoiyHBIA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/bbui" "^0.9.80-alpha.7"
|
"@budibase/bbui" "^0.9.79"
|
||||||
"@budibase/standard-components" "^0.9.80-alpha.7"
|
"@budibase/standard-components" "^0.9.79"
|
||||||
"@budibase/string-templates" "^0.9.80-alpha.7"
|
"@budibase/string-templates" "^0.9.79"
|
||||||
regexparam "^1.3.0"
|
regexparam "^1.3.0"
|
||||||
shortid "^2.2.15"
|
shortid "^2.2.15"
|
||||||
svelte-spa-router "^3.0.5"
|
svelte-spa-router "^3.0.5"
|
||||||
|
@ -1256,26 +1255,24 @@
|
||||||
to-gfm-code-block "^0.1.1"
|
to-gfm-code-block "^0.1.1"
|
||||||
year "^0.2.1"
|
year "^0.2.1"
|
||||||
|
|
||||||
"@budibase/standard-components@^0.9.80-alpha.7":
|
"@budibase/standard-components@^0.9.79", "@budibase/standard-components@^0.9.79-alpha.4":
|
||||||
version "0.9.80-alpha.7"
|
version "0.9.79"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.80-alpha.7.tgz#17f13a25bfcda873f44d1460493325adcfe6f188"
|
resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.79.tgz#24206642e0cdc655ea3a99ed5e9402ec4f6b3ba8"
|
||||||
integrity sha512-ohEVqhRxp2FeOlEnJtfBhyqtwmRGI/qPGs0K9FQfLQglMYJtPN5FgMrJ1gtN0W3zn7TOfNFnTcQIxIdLxSLwyA==
|
integrity sha512-ZWhmBZ1iG+CjGMEvT/jtugMMgA1n88UYcOfP3BSP2P3eA16DubyU9hH9OyJHbGPzDHLoBF6vuS/5ZPZCkOKppw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/bbui" "^0.9.80-alpha.7"
|
"@budibase/bbui" "^0.9.79"
|
||||||
"@spectrum-css/card" "^3.0.3"
|
|
||||||
"@spectrum-css/link" "^3.1.3"
|
"@spectrum-css/link" "^3.1.3"
|
||||||
"@spectrum-css/page" "^3.0.1"
|
"@spectrum-css/page" "^3.0.1"
|
||||||
"@spectrum-css/typography" "^3.0.2"
|
|
||||||
"@spectrum-css/vars" "^3.0.1"
|
"@spectrum-css/vars" "^3.0.1"
|
||||||
apexcharts "^3.22.1"
|
apexcharts "^3.22.1"
|
||||||
dayjs "^1.10.5"
|
dayjs "^1.10.5"
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/string-templates@^0.9.80-alpha.7":
|
"@budibase/string-templates@^0.9.79", "@budibase/string-templates@^0.9.79-alpha.4":
|
||||||
version "0.9.80-alpha.7"
|
version "0.9.79"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.80-alpha.7.tgz#10b06fc8652c00065f8928caebfcd0d143660078"
|
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.79.tgz#bb75a7433a7cfda1fc488283f35e47879b799fcc"
|
||||||
integrity sha512-lD3BSWXW6PrdAbZcpVXSsr/fA8NdwvQ8W7T4chQ661UUMKVOYLnGwAvvAOArGpkdzSOAfSEuzgIB0+pBc92qWQ==
|
integrity sha512-hkAne5mx7mj8+osXFt45VwgLKSa94uQOGOb4R8uv9WNzvk4RzcjBfRzJxggv29FUemItrAeZpSh+Um6yugFI+w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/handlebars-helpers" "^0.11.4"
|
"@budibase/handlebars-helpers" "^0.11.4"
|
||||||
dayjs "^1.10.4"
|
dayjs "^1.10.4"
|
||||||
|
@ -2114,11 +2111,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-3.0.3.tgz#719d868845ac9d2c4f939c1b9f6044507902d5aa"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/buttongroup/-/buttongroup-3.0.3.tgz#719d868845ac9d2c4f939c1b9f6044507902d5aa"
|
||||||
integrity sha512-eXl8U4QWMWXqyTu654FdQdEGnmczgOYlpIFSHyCMVjhtPqZp2xwnLFiGh6LKw+bLio6eeOZ0L+vpk1GcoYqgkw==
|
integrity sha512-eXl8U4QWMWXqyTu654FdQdEGnmczgOYlpIFSHyCMVjhtPqZp2xwnLFiGh6LKw+bLio6eeOZ0L+vpk1GcoYqgkw==
|
||||||
|
|
||||||
"@spectrum-css/card@^3.0.3":
|
|
||||||
version "3.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/card/-/card-3.0.3.tgz#56b2e2da6b80c1583228baa279de7407383bfb6b"
|
|
||||||
integrity sha512-+oKLUI2a0QmQP9EzySeq/G4FpUkkdaDNbuEbqCj2IkPMc/2v/nwzsPhh1fj2UIghGAiiUwXfPpzax1e8fyhQUg==
|
|
||||||
|
|
||||||
"@spectrum-css/checkbox@^3.0.2":
|
"@spectrum-css/checkbox@^3.0.2":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.3.tgz#8577067fc8b97e4609f92bd242364937a533a7bb"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.3.tgz#8577067fc8b97e4609f92bd242364937a533a7bb"
|
||||||
|
@ -2278,7 +2270,7 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/treeview/-/treeview-3.0.3.tgz#aeda5175158b9f8d7529cb2b394428eb2a428046"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/treeview/-/treeview-3.0.3.tgz#aeda5175158b9f8d7529cb2b394428eb2a428046"
|
||||||
integrity sha512-D5gGzZC/KtRArdx86Mesc9+99W9nTbUOeyYGqoJoAfJSOttoT6Tk5CrDvlCmAqjKf5rajemAkGri1ChqvUIwkw==
|
integrity sha512-D5gGzZC/KtRArdx86Mesc9+99W9nTbUOeyYGqoJoAfJSOttoT6Tk5CrDvlCmAqjKf5rajemAkGri1ChqvUIwkw==
|
||||||
|
|
||||||
"@spectrum-css/typography@^3.0.1", "@spectrum-css/typography@^3.0.2":
|
"@spectrum-css/typography@^3.0.1":
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.2.tgz#ea3ca0a60e18064527819d48c8c4364cab4fcd38"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/typography/-/typography-3.0.2.tgz#ea3ca0a60e18064527819d48c8c4364cab4fcd38"
|
||||||
integrity sha512-5ZOLmQe0edzsDMyhghUd4hBb5uxGsFrxzf+WasfcUw9klSfTsRZ09n1BsaaWbgrLjlMQ+EEHS46v5VNo0Ms2CA==
|
integrity sha512-5ZOLmQe0edzsDMyhghUd4hBb5uxGsFrxzf+WasfcUw9klSfTsRZ09n1BsaaWbgrLjlMQ+EEHS46v5VNo0Ms2CA==
|
||||||
|
@ -2300,17 +2292,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect "^1.0.1"
|
defer-to-connect "^1.0.1"
|
||||||
|
|
||||||
"@techpass/passport-openidconnect@^0.3.0":
|
|
||||||
version "0.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010"
|
|
||||||
integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw==
|
|
||||||
dependencies:
|
|
||||||
base64url "^3.0.1"
|
|
||||||
oauth "^0.9.15"
|
|
||||||
passport-strategy "^1.0.0"
|
|
||||||
request "^2.88.0"
|
|
||||||
webfinger "^0.4.2"
|
|
||||||
|
|
||||||
"@tootallnate/once@1":
|
"@tootallnate/once@1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
|
@ -3305,7 +3286,7 @@ base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
base64url@3.x.x, base64url@^3.0.1:
|
base64url@3.x.x:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
||||||
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
||||||
|
@ -8726,7 +8707,7 @@ oauth-sign@~0.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||||
|
|
||||||
oauth@0.9.x, oauth@^0.9.15:
|
oauth@0.9.x:
|
||||||
version "0.9.15"
|
version "0.9.15"
|
||||||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
||||||
|
@ -10100,7 +10081,7 @@ request-promise-native@^1.0.5:
|
||||||
stealthy-require "^1.1.1"
|
stealthy-require "^1.1.1"
|
||||||
tough-cookie "^2.3.3"
|
tough-cookie "^2.3.3"
|
||||||
|
|
||||||
"request@>= 2.52.0", request@^2.72.0, request@^2.74.0, request@^2.87.0, request@^2.88.0:
|
"request@>= 2.52.0", request@^2.72.0, request@^2.74.0, request@^2.87.0:
|
||||||
version "2.88.2"
|
version "2.88.2"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||||
|
@ -10224,7 +10205,7 @@ rimraf@2.6.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
rimraf@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
@ -10309,7 +10290,7 @@ sax@1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
||||||
|
|
||||||
sax@>=0.1.1, sax@>=0.6.0, sax@^1.2.4:
|
sax@>=0.6.0, sax@^1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
@ -10764,11 +10745,6 @@ stealthy-require@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
||||||
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
||||||
|
|
||||||
step@0.0.x:
|
|
||||||
version "0.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
|
|
||||||
integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI=
|
|
||||||
|
|
||||||
strict-uri-encode@^1.0.0:
|
strict-uri-encode@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||||
|
@ -11624,6 +11600,11 @@ untildify@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
||||||
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
||||||
|
|
||||||
|
update-dotenv@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/update-dotenv/-/update-dotenv-1.1.1.tgz#17146f302f216c3c92419d5a327a45be910050ca"
|
||||||
|
integrity sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ==
|
||||||
|
|
||||||
update-notifier@^4.1.0:
|
update-notifier@^4.1.0:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3"
|
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3"
|
||||||
|
@ -11805,14 +11786,6 @@ walker@^1.0.7, walker@~1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
makeerror "1.0.x"
|
makeerror "1.0.x"
|
||||||
|
|
||||||
webfinger@^0.4.2:
|
|
||||||
version "0.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d"
|
|
||||||
integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520=
|
|
||||||
dependencies:
|
|
||||||
step "0.0.x"
|
|
||||||
xml2js "0.1.x"
|
|
||||||
|
|
||||||
webidl-conversions@^4.0.2:
|
webidl-conversions@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||||
|
@ -12019,13 +11992,6 @@ xml-parse-from-string@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
|
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
|
||||||
integrity sha1-qQKekp09vN7RafPG4oI42VpdWig=
|
integrity sha1-qQKekp09vN7RafPG4oI42VpdWig=
|
||||||
|
|
||||||
xml2js@0.1.x:
|
|
||||||
version "0.1.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c"
|
|
||||||
integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw=
|
|
||||||
dependencies:
|
|
||||||
sax ">=0.1.1"
|
|
||||||
|
|
||||||
xml2js@0.4.19:
|
xml2js@0.4.19:
|
||||||
version "0.4.19"
|
version "0.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
"build:docker": "docker build . -t worker-service",
|
"build:docker": "docker build . -t worker-service",
|
||||||
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
||||||
"dev:builder": "npm run dev:stack:init && nodemon src/index.js",
|
"dev:builder": "npm run dev:stack:init && nodemon src/index.js",
|
||||||
"test": "jest --runInBand"
|
"test": "jest --runInBand",
|
||||||
|
"multi:enable": "node scripts/multiTenancy.js enable",
|
||||||
|
"multi:disable": "node scripts/multiTenancy.js disable"
|
||||||
},
|
},
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
@ -52,7 +54,8 @@
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pouchdb-adapter-memory": "^7.2.2",
|
"pouchdb-adapter-memory": "^7.2.2",
|
||||||
"supertest": "^6.1.3"
|
"supertest": "^6.1.3",
|
||||||
|
"update-dotenv": "^1.1.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
|
|
|
@ -4,6 +4,7 @@ const fs = require("fs")
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const envFilePath = path.join(process.cwd(), ".env")
|
const envFilePath = path.join(process.cwd(), ".env")
|
||||||
|
if (!fs.existsSync(envFilePath)) {
|
||||||
const envFileJson = {
|
const envFileJson = {
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
PORT: 4002,
|
PORT: 4002,
|
||||||
|
@ -12,12 +13,12 @@ async function init() {
|
||||||
INTERNAL_API_KEY: "budibase",
|
INTERNAL_API_KEY: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
|
||||||
REDIS_URL: "localhost:6379",
|
REDIS_URL: "localhost:6379",
|
||||||
REDIS_PASSWORD: "budibase",
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
|
// empty string is false
|
||||||
|
MULTI_TENANCY: "",
|
||||||
}
|
}
|
||||||
let envFile = ""
|
let envFile = ""
|
||||||
Object.keys(envFileJson).forEach(key => {
|
Object.keys(envFileJson).forEach(key => {
|
||||||
|
@ -25,6 +26,7 @@ async function init() {
|
||||||
})
|
})
|
||||||
fs.writeFileSync(envFilePath, envFile)
|
fs.writeFileSync(envFilePath, envFile)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if more than init required use this to determine the command type
|
// if more than init required use this to determine the command type
|
||||||
//const managementCommand = process.argv.slice(2)[0]
|
//const managementCommand = process.argv.slice(2)[0]
|
||||||
|
|
|
@ -3,3 +3,4 @@ const env = require("../src/environment")
|
||||||
env._set("NODE_ENV", "jest")
|
env._set("NODE_ENV", "jest")
|
||||||
env._set("JWT_SECRET", "test-jwtsecret")
|
env._set("JWT_SECRET", "test-jwtsecret")
|
||||||
env._set("LOG_LEVEL", "silent")
|
env._set("LOG_LEVEL", "silent")
|
||||||
|
env._set("MULTI_TENANCY", true)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const updateDotEnv = require("update-dotenv")
|
||||||
|
|
||||||
|
const arg = process.argv.slice(2)[0]
|
||||||
|
|
||||||
|
updateDotEnv({
|
||||||
|
MULTI_TENANCY: arg === "enable" ? "1" : "",
|
||||||
|
}).then(() => console.log("Updated worker!"))
|
|
@ -1,18 +1,12 @@
|
||||||
const { DocumentTypes } = require("@budibase/auth").db
|
const { getAllApps } = require("@budibase/auth/db")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
|
|
||||||
const APP_PREFIX = "app_"
|
|
||||||
const URL_REGEX_SLASH = /\/|\\/g
|
const URL_REGEX_SLASH = /\/|\\/g
|
||||||
|
|
||||||
exports.getApps = async ctx => {
|
exports.getApps = async ctx => {
|
||||||
// allDbs call of CouchDB is very inaccurate in production
|
const tenantId = ctx.user.tenantId
|
||||||
const allDbs = await CouchDB.allDbs()
|
const apps = await getAllApps(CouchDB, { tenantId })
|
||||||
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
|
|
||||||
const appPromises = appDbNames.map(db =>
|
|
||||||
new CouchDB(db).get(DocumentTypes.APP_METADATA)
|
|
||||||
)
|
|
||||||
|
|
||||||
const apps = await Promise.allSettled(appPromises)
|
|
||||||
const body = {}
|
const body = {}
|
||||||
for (let app of apps) {
|
for (let app of apps) {
|
||||||
if (app.status !== "fulfilled") {
|
if (app.status !== "fulfilled") {
|
||||||
|
|
|
@ -2,15 +2,27 @@ const authPkg = require("@budibase/auth")
|
||||||
const { google } = require("@budibase/auth/src/middleware")
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
const { oidc } = require("@budibase/auth/src/middleware")
|
const { oidc } = require("@budibase/auth/src/middleware")
|
||||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||||
const CouchDB = require("../../../db")
|
|
||||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } =
|
const { setCookie, getCookie, clearCookie, getGlobalUserByEmail, hash } =
|
||||||
authPkg.utils
|
authPkg.utils
|
||||||
const { Cookies } = authPkg.constants
|
const { Cookies } = authPkg.constants
|
||||||
const { passport } = authPkg.auth
|
const { passport } = authPkg.auth
|
||||||
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||||
|
const {
|
||||||
|
getGlobalDB,
|
||||||
|
getTenantId,
|
||||||
|
isMultiTenant,
|
||||||
|
} = require("@budibase/auth/tenancy")
|
||||||
|
const env = require("../../../environment")
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
function googleCallbackUrl() {
|
||||||
|
let callbackUrl = `/api/global/auth`
|
||||||
|
if (isMultiTenant()) {
|
||||||
|
callbackUrl += `/${getTenantId()}`
|
||||||
|
}
|
||||||
|
callbackUrl += `/google/callback`
|
||||||
|
return callbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
async function authInternal(ctx, user, err = null, info = null) {
|
async function authInternal(ctx, user, err = null, info = null) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -66,6 +78,7 @@ exports.reset = async ctx => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
// don't throw any kind of error to the user, this might give away something
|
// don't throw any kind of error to the user, this might give away something
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
@ -80,7 +93,7 @@ exports.resetUpdate = async ctx => {
|
||||||
const { resetCode, password } = ctx.request.body
|
const { resetCode, password } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
const userId = await checkResetPasswordCode(resetCode)
|
const userId = await checkResetPasswordCode(resetCode)
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const user = await db.get(userId)
|
const user = await db.get(userId)
|
||||||
user.password = await hash(password)
|
user.password = await hash(password)
|
||||||
await db.put(user)
|
await db.put(user)
|
||||||
|
@ -102,12 +115,14 @@ exports.logout = async ctx => {
|
||||||
* On a successful login, you will be redirected to the googleAuth callback route.
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
*/
|
*/
|
||||||
exports.googlePreAuth = async (ctx, next) => {
|
exports.googlePreAuth = async (ctx, next) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
let callbackUrl = googleCallbackUrl()
|
||||||
|
|
||||||
const config = await authPkg.db.getScopedConfig(db, {
|
const config = await authPkg.db.getScopedConfig(db, {
|
||||||
type: Configs.GOOGLE,
|
type: Configs.GOOGLE,
|
||||||
group: ctx.query.group,
|
workspace: ctx.query.workspace,
|
||||||
})
|
})
|
||||||
const strategy = await google.strategyFactory(config)
|
const strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
scope: ["profile", "email"],
|
scope: ["profile", "email"],
|
||||||
|
@ -115,13 +130,14 @@ exports.googlePreAuth = async (ctx, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.googleAuth = async (ctx, next) => {
|
exports.googleAuth = async (ctx, next) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
const callbackUrl = googleCallbackUrl()
|
||||||
|
|
||||||
const config = await authPkg.db.getScopedConfig(db, {
|
const config = await authPkg.db.getScopedConfig(db, {
|
||||||
type: Configs.GOOGLE,
|
type: Configs.GOOGLE,
|
||||||
group: ctx.query.group,
|
workspace: ctx.query.workspace,
|
||||||
})
|
})
|
||||||
const strategy = await google.strategyFactory(config)
|
const strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
|
|
||||||
return passport.authenticate(
|
return passport.authenticate(
|
||||||
strategy,
|
strategy,
|
||||||
|
@ -135,8 +151,7 @@ exports.googleAuth = async (ctx, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function oidcStrategyFactory(ctx, configId) {
|
async function oidcStrategyFactory(ctx, configId) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
|
||||||
const config = await authPkg.db.getScopedConfig(db, {
|
const config = await authPkg.db.getScopedConfig(db, {
|
||||||
type: Configs.OIDC,
|
type: Configs.OIDC,
|
||||||
group: ctx.query.group,
|
group: ctx.query.group,
|
||||||
|
@ -144,9 +159,12 @@ async function oidcStrategyFactory(ctx, configId) {
|
||||||
|
|
||||||
const chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
|
const chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
|
||||||
|
|
||||||
// require https callback in production
|
const protocol = env.NODE_ENV === "production" ? "https" : "http"
|
||||||
const protocol = process.env.NODE_ENV === "production" ? "https" : "http"
|
let callbackUrl = `${protocol}://${ctx.host}/api/global/auth`
|
||||||
const callbackUrl = `${protocol}://${ctx.host}/api/admin/auth/oidc/callback`
|
if (isMultiTenant()) {
|
||||||
|
callbackUrl += `/${getTenantId()}`
|
||||||
|
}
|
||||||
|
callbackUrl += `/oidc/callback`
|
||||||
|
|
||||||
return oidc.strategyFactory(chosenConfig, callbackUrl)
|
return oidc.strategyFactory(chosenConfig, callbackUrl)
|
||||||
}
|
}
|
|
@ -1,28 +1,25 @@
|
||||||
const CouchDB = require("../../../db")
|
|
||||||
const {
|
const {
|
||||||
generateConfigID,
|
generateConfigID,
|
||||||
StaticDatabases,
|
|
||||||
getConfigParams,
|
getConfigParams,
|
||||||
getGlobalUserParams,
|
getGlobalUserParams,
|
||||||
getScopedFullConfig,
|
getScopedFullConfig,
|
||||||
} = require("@budibase/auth").db
|
getAllApps,
|
||||||
|
} = require("@budibase/auth/db")
|
||||||
const { Configs } = require("../../../constants")
|
const { Configs } = require("../../../constants")
|
||||||
const email = require("../../../utilities/email")
|
const email = require("../../../utilities/email")
|
||||||
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
|
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
const APP_PREFIX = "app_"
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const { type, group, user, config } = ctx.request.body
|
const { type, workspace, user, config } = ctx.request.body
|
||||||
|
|
||||||
// Config does not exist yet
|
// Config does not exist yet
|
||||||
if (!ctx.request.body._id) {
|
if (!ctx.request.body._id) {
|
||||||
ctx.request.body._id = generateConfigID({
|
ctx.request.body._id = generateConfigID({
|
||||||
type,
|
type,
|
||||||
group,
|
workspace,
|
||||||
user,
|
user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -51,7 +48,7 @@ exports.save = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getConfigParams(
|
getConfigParams(
|
||||||
{ type: ctx.params.type },
|
{ type: ctx.params.type },
|
||||||
|
@ -65,17 +62,19 @@ exports.fetch = async function (ctx) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the most granular config for a particular configuration type.
|
* Gets the most granular config for a particular configuration type.
|
||||||
* The hierarchy is type -> group -> user.
|
* The hierarchy is type -> workspace -> user.
|
||||||
*/
|
*/
|
||||||
exports.find = async function (ctx) {
|
exports.find = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
|
||||||
const { userId, groupId } = ctx.query
|
const { userId, workspaceId } = ctx.query
|
||||||
if (groupId && userId) {
|
if (workspaceId && userId) {
|
||||||
const group = await db.get(groupId)
|
const workspace = await db.get(workspaceId)
|
||||||
const userInGroup = group.users.some(groupUser => groupUser === userId)
|
const userInWorkspace = workspace.users.some(
|
||||||
if (!ctx.user.admin && !userInGroup) {
|
workspaceUser => workspaceUser === userId
|
||||||
ctx.throw(400, `User is not in specified group: ${group}.`)
|
)
|
||||||
|
if (!ctx.user.admin && !userInWorkspace) {
|
||||||
|
ctx.throw(400, `User is not in specified workspace: ${workspace}.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +83,7 @@ exports.find = async function (ctx) {
|
||||||
const scopedConfig = await getScopedFullConfig(db, {
|
const scopedConfig = await getScopedFullConfig(db, {
|
||||||
type: ctx.params.type,
|
type: ctx.params.type,
|
||||||
user: userId,
|
user: userId,
|
||||||
group: groupId,
|
workspace: workspaceId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (scopedConfig) {
|
if (scopedConfig) {
|
||||||
|
@ -99,7 +98,7 @@ exports.find = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.publicOidc = async function (ctx) {
|
exports.publicOidc = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
try {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// Find the config with the most granular scope based on context
|
||||||
const oidcConfig = await getScopedFullConfig(db, {
|
const oidcConfig = await getScopedFullConfig(db, {
|
||||||
|
@ -109,14 +108,11 @@ exports.publicOidc = async function (ctx) {
|
||||||
if (!oidcConfig) {
|
if (!oidcConfig) {
|
||||||
ctx.body = {}
|
ctx.body = {}
|
||||||
} else {
|
} else {
|
||||||
const partialOidcCofig = oidcConfig.config.configs.map(config => {
|
ctx.body = oidcConfig.config.configs.map(config => ({
|
||||||
return {
|
|
||||||
logo: config.logo,
|
logo: config.logo,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
uuid: config.uuid,
|
uuid: config.uuid,
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
ctx.body = partialOidcCofig
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
|
@ -124,7 +120,7 @@ exports.publicOidc = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.publicSettings = async function (ctx) {
|
exports.publicSettings = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// Find the config with the most granular scope based on context
|
||||||
|
@ -140,7 +136,7 @@ exports.publicSettings = async function (ctx) {
|
||||||
type: Configs.OIDC,
|
type: Configs.OIDC,
|
||||||
})
|
})
|
||||||
|
|
||||||
let config = {}
|
let config
|
||||||
if (!publicConfig) {
|
if (!publicConfig) {
|
||||||
config = {
|
config = {
|
||||||
config: {},
|
config: {},
|
||||||
|
@ -151,18 +147,16 @@ exports.publicSettings = async function (ctx) {
|
||||||
|
|
||||||
// google button flag
|
// google button flag
|
||||||
if (googleConfig && googleConfig.config) {
|
if (googleConfig && googleConfig.config) {
|
||||||
const googleActivated =
|
// activated by default for configs pre-activated flag
|
||||||
googleConfig.config.activated == undefined || // activated by default for configs pre-activated flag
|
config.config.google =
|
||||||
googleConfig.config.activated
|
googleConfig.config.activated == null || googleConfig.config.activated
|
||||||
config.config.google = googleActivated
|
|
||||||
} else {
|
} else {
|
||||||
config.config.google = false
|
config.config.google = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// oidc button flag
|
// oidc button flag
|
||||||
if (oidcConfig && oidcConfig.config) {
|
if (oidcConfig && oidcConfig.config) {
|
||||||
const oidcActivated = oidcConfig.config.configs[0].activated
|
config.config.oidc = oidcConfig.config.configs[0].activated
|
||||||
config.config.oidc = oidcActivated
|
|
||||||
} else {
|
} else {
|
||||||
config.config.oidc = false
|
config.config.oidc = false
|
||||||
}
|
}
|
||||||
|
@ -191,7 +185,7 @@ exports.upload = async function (ctx) {
|
||||||
|
|
||||||
// add to configuration structure
|
// add to configuration structure
|
||||||
// TODO: right now this only does a global level
|
// TODO: right now this only does a global level
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
let cfgStructure = await getScopedFullConfig(db, { type })
|
let cfgStructure = await getScopedFullConfig(db, { type })
|
||||||
if (!cfgStructure) {
|
if (!cfgStructure) {
|
||||||
cfgStructure = {
|
cfgStructure = {
|
||||||
|
@ -211,7 +205,7 @@ exports.upload = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async function (ctx) {
|
exports.destroy = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const { id, rev } = ctx.params
|
const { id, rev } = ctx.params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -223,14 +217,13 @@ exports.destroy = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.configChecklist = async function (ctx) {
|
exports.configChecklist = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Watch get started video
|
// TODO: Watch get started video
|
||||||
|
|
||||||
// Apps exist
|
// Apps exist
|
||||||
let allDbs = await CouchDB.allDbs()
|
const apps = await getAllApps(CouchDB)
|
||||||
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
|
|
||||||
|
|
||||||
// They have set up SMTP
|
// They have set up SMTP
|
||||||
const smtpConfig = await getScopedFullConfig(db, {
|
const smtpConfig = await getScopedFullConfig(db, {
|
||||||
|
@ -246,7 +239,7 @@ exports.configChecklist = async function (ctx) {
|
||||||
const oidcConfig = await getScopedFullConfig(db, {
|
const oidcConfig = await getScopedFullConfig(db, {
|
||||||
type: Configs.OIDC,
|
type: Configs.OIDC,
|
||||||
})
|
})
|
||||||
// They have set up an admin user
|
// They have set up an global user
|
||||||
const users = await db.allDocs(
|
const users = await db.allDocs(
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -255,7 +248,7 @@ exports.configChecklist = async function (ctx) {
|
||||||
const adminUser = users.rows.some(row => row.doc.admin)
|
const adminUser = users.rows.some(row => row.doc.admin)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
apps: appDbNames.length,
|
apps: apps.length,
|
||||||
smtp: !!smtpConfig,
|
smtp: !!smtpConfig,
|
||||||
adminUser,
|
adminUser,
|
||||||
sso: !!googleConfig || !!oidcConfig,
|
sso: !!googleConfig || !!oidcConfig,
|
|
@ -1,19 +1,16 @@
|
||||||
const { sendEmail } = require("../../../utilities/email")
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
const CouchDB = require("../../../db")
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
const authPkg = require("@budibase/auth")
|
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
|
||||||
|
|
||||||
exports.sendEmail = async ctx => {
|
exports.sendEmail = async ctx => {
|
||||||
const { groupId, email, userId, purpose, contents, from, subject } =
|
let { workspaceId, email, userId, purpose, contents, from, subject } =
|
||||||
ctx.request.body
|
ctx.request.body
|
||||||
let user
|
let user
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
user = await db.get(userId)
|
user = await db.get(userId)
|
||||||
}
|
}
|
||||||
const response = await sendEmail(email, purpose, {
|
const response = await sendEmail(email, purpose, {
|
||||||
groupId,
|
workspaceId,
|
||||||
user,
|
user,
|
||||||
contents,
|
contents,
|
||||||
from,
|
from,
|
|
@ -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(CouchDB, { 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
|
|
@ -1,16 +1,14 @@
|
||||||
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
const { generateTemplateID } = require("@budibase/auth/db")
|
||||||
const CouchDB = require("../../../db")
|
|
||||||
const {
|
const {
|
||||||
TemplateMetadata,
|
TemplateMetadata,
|
||||||
TemplateBindings,
|
TemplateBindings,
|
||||||
GLOBAL_OWNER,
|
GLOBAL_OWNER,
|
||||||
} = require("../../../constants")
|
} = require("../../../constants")
|
||||||
const { getTemplates } = require("../../../constants/templates")
|
const { getTemplates } = require("../../../constants/templates")
|
||||||
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
let template = ctx.request.body
|
let template = ctx.request.body
|
||||||
if (!template.ownerId) {
|
if (!template.ownerId) {
|
||||||
template.ownerId = GLOBAL_OWNER
|
template.ownerId = GLOBAL_OWNER
|
||||||
|
@ -70,7 +68,7 @@ exports.find = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async ctx => {
|
exports.destroy = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
await db.remove(ctx.params.id, ctx.params.rev)
|
await db.remove(ctx.params.id, ctx.params.rev)
|
||||||
ctx.message = `Template ${ctx.params.id} deleted.`
|
ctx.message = `Template ${ctx.params.id} deleted.`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
|
@ -1,17 +1,30 @@
|
||||||
const CouchDB = require("../../../db")
|
const {
|
||||||
const { generateGlobalUserID, getGlobalUserParams, StaticDatabases } =
|
generateGlobalUserID,
|
||||||
require("@budibase/auth").db
|
getGlobalUserParams,
|
||||||
|
|
||||||
|
StaticDatabases,
|
||||||
|
} = require("@budibase/auth/db")
|
||||||
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||||
const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
|
const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
const { DEFAULT_TENANT_ID } = require("@budibase/auth/constants")
|
||||||
const { checkInviteCode } = require("../../../utilities/redis")
|
const { checkInviteCode } = require("../../../utilities/redis")
|
||||||
const { sendEmail } = require("../../../utilities/email")
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
const { user: userCache } = require("@budibase/auth/cache")
|
const { user: userCache } = require("@budibase/auth/cache")
|
||||||
const { invalidateSessions } = require("@budibase/auth/sessions")
|
const { invalidateSessions } = require("@budibase/auth/sessions")
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const env = require("../../../environment")
|
||||||
|
const {
|
||||||
|
getGlobalDB,
|
||||||
|
getTenantId,
|
||||||
|
doesTenantExist,
|
||||||
|
tryAddTenant,
|
||||||
|
updateTenantId,
|
||||||
|
} = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||||
|
|
||||||
async function allUsers() {
|
async function allUsers() {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -20,16 +33,21 @@ async function allUsers() {
|
||||||
return response.rows.map(row => row.doc)
|
return response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async ctx => {
|
async function saveUser(user, tenantId) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
if (!tenantId) {
|
||||||
const { email, password, _id } = ctx.request.body
|
throw "No tenancy specified."
|
||||||
|
}
|
||||||
|
// need to set the context for this request, as specified
|
||||||
|
updateTenantId(tenantId)
|
||||||
|
// specify the tenancy incase we're making a new admin user (public)
|
||||||
|
const db = getGlobalDB(tenantId)
|
||||||
|
let { email, password, _id } = user
|
||||||
// make sure another user isn't using the same email
|
// make sure another user isn't using the same email
|
||||||
let dbUser
|
let dbUser
|
||||||
if (email) {
|
if (email) {
|
||||||
dbUser = await getGlobalUserByEmail(email)
|
dbUser = await getGlobalUserByEmail(email)
|
||||||
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
throw "Email address already in use."
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dbUser = await db.get(_id)
|
dbUser = await db.get(_id)
|
||||||
|
@ -42,14 +60,16 @@ exports.save = async ctx => {
|
||||||
} else if (dbUser) {
|
} else if (dbUser) {
|
||||||
hashedPassword = dbUser.password
|
hashedPassword = dbUser.password
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(400, "Password must be specified.")
|
throw "Password must be specified."
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = {
|
_id = _id || generateGlobalUserID()
|
||||||
|
user = {
|
||||||
...dbUser,
|
...dbUser,
|
||||||
...ctx.request.body,
|
...user,
|
||||||
_id: _id || generateGlobalUserID(),
|
_id,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
tenantId,
|
||||||
}
|
}
|
||||||
// make sure the roles object is always present
|
// make sure the roles object is always present
|
||||||
if (!user.roles) {
|
if (!user.roles) {
|
||||||
|
@ -64,23 +84,37 @@ exports.save = async ctx => {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
...user,
|
...user,
|
||||||
})
|
})
|
||||||
|
await tryAddTenant(tenantId, _id, email)
|
||||||
await userCache.invalidateUser(response.id)
|
await userCache.invalidateUser(response.id)
|
||||||
ctx.body = {
|
return {
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
email,
|
email,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 409) {
|
if (err.status === 409) {
|
||||||
ctx.throw(400, "User exists already")
|
throw "User exists already"
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(err.status, err)
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.save = async ctx => {
|
||||||
|
try {
|
||||||
|
ctx.body = await saveUser(ctx.request.body, getTenantId())
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status || 400, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.adminUser = async ctx => {
|
exports.adminUser = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const { email, password, tenantId } = ctx.request.body
|
||||||
|
if (await doesTenantExist(tenantId)) {
|
||||||
|
ctx.throw(403, "Organisation already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getGlobalDB(tenantId)
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -88,11 +122,13 @@ exports.adminUser = async ctx => {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.rows.some(row => row.doc.admin)) {
|
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 global user has been created."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password } = ctx.request.body
|
const user = {
|
||||||
ctx.request.body = {
|
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
roles: {},
|
roles: {},
|
||||||
|
@ -102,12 +138,17 @@ exports.adminUser = async ctx => {
|
||||||
admin: {
|
admin: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
|
tenantId,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ctx.body = await saveUser(user, tenantId)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status || 400, err)
|
||||||
}
|
}
|
||||||
await exports.save(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async ctx => {
|
exports.destroy = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const dbUser = await db.get(ctx.params.id)
|
const dbUser = await db.get(ctx.params.id)
|
||||||
await db.remove(dbUser._id, dbUser._rev)
|
await db.remove(dbUser._id, dbUser._rev)
|
||||||
await userCache.invalidateUser(dbUser._id)
|
await userCache.invalidateUser(dbUser._id)
|
||||||
|
@ -119,8 +160,8 @@ exports.destroy = async ctx => {
|
||||||
|
|
||||||
exports.removeAppRole = async ctx => {
|
exports.removeAppRole = async ctx => {
|
||||||
const { appId } = ctx.params
|
const { appId } = ctx.params
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const users = await allUsers()
|
const users = await allUsers(ctx)
|
||||||
const bulk = []
|
const bulk = []
|
||||||
const cacheInvalidations = []
|
const cacheInvalidations = []
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
|
@ -149,7 +190,7 @@ exports.getSelf = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateSelf = async ctx => {
|
exports.updateSelf = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const user = await db.get(ctx.user._id)
|
const user = await db.get(ctx.user._id)
|
||||||
if (ctx.request.body.password) {
|
if (ctx.request.body.password) {
|
||||||
ctx.request.body.password = await hash(ctx.request.body.password)
|
ctx.request.body.password = await hash(ctx.request.body.password)
|
||||||
|
@ -170,7 +211,7 @@ exports.updateSelf = async ctx => {
|
||||||
|
|
||||||
// called internally by app server user fetch
|
// called internally by app server user fetch
|
||||||
exports.fetch = async ctx => {
|
exports.fetch = async ctx => {
|
||||||
const users = await allUsers()
|
const users = await allUsers(ctx)
|
||||||
// user hashed password shouldn't ever be returned
|
// user hashed password shouldn't ever be returned
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -182,7 +223,7 @@ exports.fetch = async ctx => {
|
||||||
|
|
||||||
// called internally by app server user find
|
// called internally by app server user find
|
||||||
exports.find = async ctx => {
|
exports.find = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
let user
|
let user
|
||||||
try {
|
try {
|
||||||
user = await db.get(ctx.params.id)
|
user = await db.get(ctx.params.id)
|
||||||
|
@ -196,12 +237,38 @@ exports.find = async ctx => {
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.tenantLookup = async ctx => {
|
||||||
|
const id = ctx.params.id
|
||||||
|
// lookup, could be email or userId, either will return a doc
|
||||||
|
const db = new CouchDB(PLATFORM_INFO_DB)
|
||||||
|
let tenantId = null
|
||||||
|
try {
|
||||||
|
const doc = await db.get(id)
|
||||||
|
if (doc && doc.tenantId) {
|
||||||
|
tenantId = doc.tenantId
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!env.MULTI_TENANCY) {
|
||||||
|
tenantId = DEFAULT_TENANT_ID
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, "No tenant found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
tenantId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.invite = async ctx => {
|
exports.invite = async ctx => {
|
||||||
const { email, userInfo } = ctx.request.body
|
let { email, userInfo } = ctx.request.body
|
||||||
const existing = await getGlobalUserByEmail(email)
|
const existing = await getGlobalUserByEmail(email)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
ctx.throw(400, "Email address already in use.")
|
||||||
}
|
}
|
||||||
|
if (!userInfo) {
|
||||||
|
userInfo = {}
|
||||||
|
}
|
||||||
|
userInfo.tenantId = getTenantId()
|
||||||
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
||||||
subject: "{{ company }} platform invitation",
|
subject: "{{ company }} platform invitation",
|
||||||
info: userInfo,
|
info: userInfo,
|
||||||
|
@ -214,18 +281,18 @@ exports.invite = async ctx => {
|
||||||
exports.inviteAccept = async ctx => {
|
exports.inviteAccept = async ctx => {
|
||||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
// info is an extension of the user object that was stored by admin
|
// info is an extension of the user object that was stored by global
|
||||||
const { email, info } = await checkInviteCode(inviteCode)
|
const { email, info } = await checkInviteCode(inviteCode)
|
||||||
// only pass through certain props for accepting
|
ctx.body = await saveUser(
|
||||||
ctx.request.body = {
|
{
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
...info,
|
...info,
|
||||||
}
|
},
|
||||||
// this will flesh out the body response
|
info.tenantId
|
||||||
await exports.save(ctx)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
||||||
}
|
}
|
|
@ -1,20 +1,17 @@
|
||||||
const CouchDB = require("../../../db")
|
const { getWorkspaceParams, generateWorkspaceID } = require("@budibase/auth/db")
|
||||||
const { getGroupParams, generateGroupID, StaticDatabases } =
|
const { getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
require("@budibase/auth").db
|
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const groupDoc = ctx.request.body
|
const workspaceDoc = ctx.request.body
|
||||||
|
|
||||||
// Group does not exist yet
|
// workspace does not exist yet
|
||||||
if (!groupDoc._id) {
|
if (!workspaceDoc._id) {
|
||||||
groupDoc._id = generateGroupID()
|
workspaceDoc._id = generateWorkspaceID()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await db.post(groupDoc)
|
const response = await db.post(workspaceDoc)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
|
@ -25,9 +22,9 @@ exports.save = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getGroupParams(undefined, {
|
getWorkspaceParams(undefined, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -35,7 +32,7 @@ exports.fetch = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function (ctx) {
|
exports.find = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
try {
|
try {
|
||||||
ctx.body = await db.get(ctx.params.id)
|
ctx.body = await db.get(ctx.params.id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -44,12 +41,12 @@ exports.find = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async function (ctx) {
|
exports.destroy = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = getGlobalDB()
|
||||||
const { id, rev } = ctx.params
|
const { id, rev } = ctx.params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.remove(id, rev)
|
await db.remove(id, rev)
|
||||||
ctx.body = { message: "Group deleted successfully" }
|
ctx.body = { message: "Workspace deleted successfully" }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
}
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
const env = require("../../../environment")
|
||||||
|
|
||||||
|
exports.fetch = async ctx => {
|
||||||
|
ctx.body = {
|
||||||
|
multiTenancy: !!env.MULTI_TENANCY,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const { StaticDatabases } = require("@budibase/auth/db")
|
||||||
|
|
||||||
|
exports.exists = async ctx => {
|
||||||
|
const tenantId = ctx.request.params
|
||||||
|
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name)
|
||||||
|
let exists = false
|
||||||
|
try {
|
||||||
|
const tenantsDoc = await db.get(StaticDatabases.PLATFORM_INFO.docs.tenants)
|
||||||
|
if (tenantsDoc) {
|
||||||
|
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// if error it doesn't exist
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
exists,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetch = async ctx => {
|
||||||
|
const db = new CouchDB(StaticDatabases.PLATFORM_INFO.name)
|
||||||
|
let tenants = []
|
||||||
|
try {
|
||||||
|
const tenantsDoc = await db.get(StaticDatabases.PLATFORM_INFO.docs.tenants)
|
||||||
|
if (tenantsDoc) {
|
||||||
|
tenants = tenantsDoc.tenantIds
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// if error it doesn't exist
|
||||||
|
}
|
||||||
|
ctx.body = tenants
|
||||||
|
}
|
|
@ -2,55 +2,51 @@ const Router = require("@koa/router")
|
||||||
const compress = require("koa-compress")
|
const compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
const { routes } = require("./routes")
|
const { routes } = require("./routes")
|
||||||
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
|
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } =
|
||||||
|
require("@budibase/auth").auth
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
{
|
{
|
||||||
route: "/api/admin/users/init",
|
// this covers all of the POST auth routes
|
||||||
|
route: "/api/global/auth/:tenantId",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/users/invite/accept",
|
// this covers all of the GET auth routes
|
||||||
|
route: "/api/global/auth/:tenantId",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this covers all of the public config routes
|
||||||
|
route: "/api/global/configs/public",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/global/configs/checklist",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/global/users/init",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth",
|
route: "/api/global/users/invite/accept",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/google",
|
route: "api/system/flags",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const NO_TENANCY_ENDPOINTS = [
|
||||||
|
...PUBLIC_ENDPOINTS,
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/google/callback",
|
route: "/api/system",
|
||||||
method: "GET",
|
method: "ALL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth/oidc",
|
route: "/api/global/users/self",
|
||||||
method: "GET",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/admin/auth/oidc/callback",
|
|
||||||
method: "GET",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/admin/auth/reset",
|
|
||||||
method: "POST",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/admin/configs/checklist",
|
|
||||||
method: "GET",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/apps",
|
|
||||||
method: "GET",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/admin/configs/public",
|
|
||||||
method: "GET",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
route: "/api/admin/configs/publicOidc",
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -71,9 +67,10 @@ router
|
||||||
)
|
)
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
|
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (!ctx.isAuthenticated) {
|
if (!ctx.isAuthenticated && !ctx.publicEndpoint) {
|
||||||
ctx.throw(403, "Unauthorized - no public worker access")
|
ctx.throw(403, "Unauthorized - no public worker access")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const authController = require("../../controllers/admin/auth")
|
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
|
||||||
const Joi = require("joi")
|
|
||||||
|
|
||||||
const router = Router()
|
|
||||||
|
|
||||||
function buildAuthValidation() {
|
|
||||||
// prettier-ignore
|
|
||||||
return joiValidator.body(Joi.object({
|
|
||||||
username: Joi.string().required(),
|
|
||||||
password: Joi.string().required(),
|
|
||||||
}).required().unknown(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResetValidation() {
|
|
||||||
// prettier-ignore
|
|
||||||
return joiValidator.body(Joi.object({
|
|
||||||
email: Joi.string().required(),
|
|
||||||
}).required().unknown(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResetUpdateValidation() {
|
|
||||||
// prettier-ignore
|
|
||||||
return joiValidator.body(Joi.object({
|
|
||||||
resetCode: Joi.string().required(),
|
|
||||||
password: Joi.string().required(),
|
|
||||||
}).required().unknown(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
router
|
|
||||||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
|
|
||||||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
|
|
||||||
.post(
|
|
||||||
"/api/admin/auth/reset/update",
|
|
||||||
buildResetUpdateValidation(),
|
|
||||||
authController.resetUpdate
|
|
||||||
)
|
|
||||||
.post("/api/admin/auth/logout", authController.logout)
|
|
||||||
.get("/api/admin/auth/google", authController.googlePreAuth)
|
|
||||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
|
||||||
.get("/api/admin/auth/oidc/configs/:configId", authController.oidcPreAuth)
|
|
||||||
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
|
|
||||||
|
|
||||||
module.exports = router
|
|
|
@ -1,11 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const controller = require("../../controllers/admin/roles")
|
|
||||||
const adminOnly = require("../../../middleware/adminOnly")
|
|
||||||
|
|
||||||
const router = Router()
|
|
||||||
|
|
||||||
router
|
|
||||||
.get("/api/admin/roles", adminOnly, controller.fetch)
|
|
||||||
.get("/api/admin/roles/:appId", adminOnly, controller.find)
|
|
||||||
|
|
||||||
module.exports = router
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue