This commit is contained in:
Martin McKeaveney 2021-04-12 10:48:27 +01:00
commit 6499213f90
27 changed files with 292 additions and 201 deletions

View File

@ -29,7 +29,7 @@
"clean": "lerna clean", "clean": "lerna clean",
"kill-port": "kill-port 4001", "kill-port": "kill-port 4001",
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1", "dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "lerna link && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server", "dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"test": "lerna run test", "test": "lerna run test",
"lint": "eslint packages", "lint": "eslint packages",
"lint:fix": "eslint --fix packages", "lint:fix": "eslint --fix packages",

View File

@ -26,13 +26,12 @@ exports.generateUserID = email => {
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. * Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/ */
exports.getUserParams = (email = "", otherProps = {}) => { exports.getUserParams = (email = "", otherProps = {}) => {
if (!email) {
email = ""
}
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`, startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`,
endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`,
} }
} }
exports.getEmailFromUserID = id => {
return id.split(`${DocumentTypes.USER}${SEPARATOR}`)[1]
}

View File

@ -1,7 +1,7 @@
import api from "builderStore/api" import api from "builderStore/api"
export async function createUser(user) { export async function createUser(user) {
const CREATE_USER_URL = `/api/users` const CREATE_USER_URL = `/api/users/metadata`
const response = await api.post(CREATE_USER_URL, user) const response = await api.post(CREATE_USER_URL, user)
return await response.json() return await response.json()
} }
@ -15,8 +15,7 @@ export async function saveRow(row, tableId) {
export async function deleteRow(row) { export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}` const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}`
const response = await api.delete(DELETE_ROWS_URL) return api.delete(DELETE_ROWS_URL)
return response
} }
export async function fetchDataForView(view) { export async function fetchDataForView(view) {

View File

@ -157,7 +157,7 @@
password: $createAppStore.values.password, password: $createAppStore.values.password,
roleId: $createAppStore.values.roleId, roleId: $createAppStore.values.roleId,
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users/metadata`, user)
const json = await userResp.json() const json = await userResp.json()
$goto(`./${appJson._id}`) $goto(`./${appJson._id}`)
} catch (error) { } catch (error) {

View File

@ -41,6 +41,12 @@ module.exports = async (url, opts) => {
], ],
bookmark: "test", bookmark: "test",
}) })
} else if (url.includes("/api/admin")) {
return json({
email: "test@test.com",
_id: "us_test@test.com",
status: "active",
})
} }
return fetch(url, opts) return fetch(url, opts)
} }

View File

@ -74,7 +74,7 @@ async function getAppUrlIfNotInUse(ctx) {
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
return url return url
} }
const deployedApps = await getDeployedApps() const deployedApps = await getDeployedApps(ctx)
if ( if (
deployedApps[url] != null && deployedApps[url] != null &&
deployedApps[url].appId !== ctx.params.appId deployedApps[url].appId !== ctx.params.appId

View File

@ -3,10 +3,10 @@ const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt") const bcrypt = require("../../utilities/bcrypt")
const env = require("../../environment") const env = require("../../environment")
const { getAPIKey } = require("../../utilities/usageQuota") const { getAPIKey } = require("../../utilities/usageQuota")
const { generateUserID } = require("../../db/utils") const { generateUserMetadataID } = require("../../db/utils")
const { setCookie } = require("../../utilities") const { setCookie } = require("../../utilities")
const { outputProcessing } = require("../../utilities/rowProcessor") const { outputProcessing } = require("../../utilities/rowProcessor")
const { ViewNames } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
const { UserStatus } = require("@budibase/auth") const { UserStatus } = require("@budibase/auth")
const setBuilderToken = require("../../utilities/builder/setBuilderToken") const setBuilderToken = require("../../utilities/builder/setBuilderToken")
@ -27,7 +27,7 @@ exports.authenticate = async ctx => {
let dbUser let dbUser
try { try {
dbUser = await db.get(generateUserID(email)) dbUser = await db.get(generateUserMetadataID(email))
} catch (_) { } catch (_) {
// do not want to throw a 404 - as this could be // do not want to throw a 404 - as this could be
// used to determine valid emails // used to determine valid emails
@ -84,7 +84,7 @@ exports.fetchSelf = async ctx => {
} }
const db = new CouchDB(appId) const db = new CouchDB(appId)
const user = await db.get(userId) const user = await db.get(userId)
const userTable = await db.get(ViewNames.USERS) const userTable = await db.get(InternalTables.USER_METADATA)
if (user) { if (user) {
delete user.password delete user.password
} }

View File

@ -40,5 +40,5 @@ exports.fetchUrls = async ctx => {
} }
exports.getDeployedApps = async ctx => { exports.getDeployedApps = async ctx => {
ctx.body = await getDeployedApps() ctx.body = await getDeployedApps(ctx)
} }

View File

@ -10,8 +10,8 @@ const {
const { const {
generateRoleID, generateRoleID,
getRoleParams, getRoleParams,
getUserParams, getUserMetadataParams,
ViewNames, InternalTables,
} = require("../../db/utils") } = require("../../db/utils")
const UpdateRolesOptions = { const UpdateRolesOptions = {
@ -28,7 +28,7 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [
] ]
async function updateRolesOnUserTable(db, roleId, updateOption) { async function updateRolesOnUserTable(db, roleId, updateOption) {
const table = await db.get(ViewNames.USERS) const table = await db.get(InternalTables.USER_METADATA)
const schema = table.schema const schema = table.schema
const remove = updateOption === UpdateRolesOptions.REMOVED const remove = updateOption === UpdateRolesOptions.REMOVED
let updated = false let updated = false
@ -112,7 +112,7 @@ exports.destroy = async function(ctx) {
// first check no users actively attached to role // first check no users actively attached to role
const users = ( const users = (
await db.allDocs( await db.allDocs(
getUserParams(null, { getUserMetadataParams(null, {
include_docs: true, include_docs: true,
}) })
) )

View File

@ -6,10 +6,10 @@ const {
generateRowID, generateRowID,
DocumentTypes, DocumentTypes,
SEPARATOR, SEPARATOR,
ViewNames, InternalTables,
generateUserID, generateUserMetadataID,
} = require("../../db/utils") } = require("../../db/utils")
const usersController = require("./user") const userController = require("./user")
const { const {
inputProcessing, inputProcessing,
outputProcessing, outputProcessing,
@ -37,18 +37,14 @@ validateJs.extend(validateJs.validators.datetime, {
}, },
}) })
async function findRow(db, appId, tableId, rowId) { async function findRow(ctx, db, tableId, rowId) {
let row let row
if (tableId === ViewNames.USERS) { // TODO remove special user case in future
let ctx = { if (tableId === InternalTables.USER_METADATA) {
params: { ctx.params = {
userId: rowId, userId: rowId,
},
user: {
appId,
},
} }
await usersController.findMetadata(ctx) await userController.findMetadata(ctx)
row = ctx.body row = ctx.body
} else { } else {
row = await db.get(rowId) row = await db.get(rowId)
@ -96,14 +92,14 @@ exports.patch = async function(ctx) {
table, table,
}) })
// Creation of a new user goes to the user controller // TODO remove special user case in future
if (row.tableId === ViewNames.USERS) { if (row.tableId === InternalTables.USER_METADATA) {
// the row has been updated, need to put it into the ctx // the row has been updated, need to put it into the ctx
ctx.request.body = { ctx.request.body = {
...row, ...row,
password: ctx.request.body.password, password: ctx.request.body.password,
} }
await usersController.updateMetadata(ctx) await userController.updateMetadata(ctx)
return return
} }
@ -142,8 +138,9 @@ exports.save = async function(ctx) {
} }
if (!inputs._rev && !inputs._id) { if (!inputs._rev && !inputs._id) {
if (inputs.tableId === ViewNames.USERS) { // TODO remove special user case in future
inputs._id = generateUserID(inputs.email) if (inputs.tableId === InternalTables.USER_METADATA) {
inputs._id = generateUserMetadataID(inputs.email)
} else { } else {
inputs._id = generateRowID(inputs.tableId) inputs._id = generateRowID(inputs.tableId)
} }
@ -175,11 +172,11 @@ exports.save = async function(ctx) {
table, table,
}) })
// Creation of a new user goes to the user controller // TODO remove special user case in future
if (row.tableId === ViewNames.USERS) { if (row.tableId === InternalTables.USER_METADATA) {
// the row has been updated, need to put it into the ctx // the row has been updated, need to put it into the ctx
ctx.request.body = row ctx.request.body = row
await usersController.createMetadata(ctx) await userController.createMetadata(ctx)
return return
} }
@ -287,14 +284,6 @@ exports.search = async function(ctx) {
} }
const response = await search(searchString) const response = await search(searchString)
// delete passwords from users
if (tableId === ViewNames.USERS) {
for (let row of response.rows) {
delete row.password
}
}
const table = await db.get(tableId) const table = await db.get(tableId)
ctx.body = { ctx.body = {
rows: await outputProcessing(appId, table, response.rows), rows: await outputProcessing(appId, table, response.rows),
@ -306,11 +295,11 @@ exports.fetchTableRows = async function(ctx) {
const appId = ctx.appId const appId = ctx.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
// special case for users, fetch through the user controller // TODO remove special user case in future
let rows, let rows,
table = await db.get(ctx.params.tableId) table = await db.get(ctx.params.tableId)
if (ctx.params.tableId === ViewNames.USERS) { if (ctx.params.tableId === InternalTables.USER_METADATA) {
await usersController.fetchMetadata(ctx) await userController.fetchMetadata(ctx)
rows = ctx.body rows = ctx.body
} else { } else {
const response = await db.allDocs( const response = await db.allDocs(
@ -328,7 +317,7 @@ exports.find = async function(ctx) {
const db = new CouchDB(appId) const db = new CouchDB(appId)
try { try {
const table = await db.get(ctx.params.tableId) const table = await db.get(ctx.params.tableId)
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId) const row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
ctx.body = await outputProcessing(appId, table, row) ctx.body = await outputProcessing(appId, table, row)
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
@ -348,11 +337,19 @@ exports.destroy = async function(ctx) {
row, row,
tableId: row.tableId, tableId: row.tableId,
}) })
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId) // TODO remove special user case in future
ctx.status = 200 if (ctx.params.tableId === InternalTables.USER_METADATA) {
ctx.params = {
userId: ctx.params.rowId,
}
await userController.destroyMetadata(ctx)
} else {
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
}
// for automations include the row that was deleted // for automations include the row that was deleted
ctx.row = row ctx.row = row
ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
} }
@ -395,7 +392,7 @@ exports.fetchEnrichedRow = async function(ctx) {
// need table to work out where links go in row // need table to work out where links go in row
let [table, row] = await Promise.all([ let [table, row] = await Promise.all([
db.get(tableId), db.get(tableId),
findRow(db, appId, tableId, rowId), findRow(ctx, db, tableId, rowId),
]) ])
// get the link docs // get the link docs
const linkVals = await linkRows.getLinkDocuments({ const linkVals = await linkRows.getLinkDocuments({
@ -437,7 +434,7 @@ async function bulkDelete(ctx) {
const { rows } = ctx.request.body const { rows } = ctx.request.body
const db = new CouchDB(appId) const db = new CouchDB(appId)
const linkUpdates = rows.map(row => let updates = rows.map(row =>
linkRows.updateLinks({ linkRows.updateLinks({
appId, appId,
eventType: linkRows.EventType.ROW_DELETE, eventType: linkRows.EventType.ROW_DELETE,
@ -445,9 +442,20 @@ async function bulkDelete(ctx) {
tableId: row.tableId, tableId: row.tableId,
}) })
) )
// TODO remove special user case in future
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true }))) if (ctx.params.tableId === InternalTables.USER_METADATA) {
await Promise.all(linkUpdates) updates = updates.concat(
rows.map(row => {
ctx.params = {
userId: row._id,
}
return userController.destroyMetadata(ctx)
})
)
} else {
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
}
await Promise.all(updates)
rows.forEach(row => { rows.forEach(row => {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)

View File

@ -22,7 +22,7 @@ const { objectStoreUrl, clientLibraryPath } = require("../../../utilities")
async function checkForSelfHostedURL(ctx) { async function checkForSelfHostedURL(ctx) {
// the "appId" component of the URL may actually be a specific self hosted URL // the "appId" component of the URL may actually be a specific self hosted URL
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}` let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
const apps = await getDeployedApps() const apps = await getDeployedApps(ctx)
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) { if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
return apps[possibleAppUrl].appId return apps[possibleAppUrl].appId
} else { } else {

View File

@ -1,6 +1,10 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const csvParser = require("../../../utilities/csvParser") const csvParser = require("../../../utilities/csvParser")
const { getRowParams, generateRowID, ViewNames } = require("../../../db/utils") const {
getRowParams,
generateRowID,
InternalTables,
} = require("../../../db/utils")
const { isEqual } = require("lodash/fp") const { isEqual } = require("lodash/fp")
const { AutoFieldSubTypes } = require("../../../constants") const { AutoFieldSubTypes } = require("../../../constants")
const { inputProcessing } = require("../../../utilities/rowProcessor") const { inputProcessing } = require("../../../utilities/rowProcessor")
@ -136,7 +140,7 @@ exports.handleSearchIndexes = async (appId, table) => {
exports.checkStaticTables = table => { exports.checkStaticTables = table => {
// check user schema has all required elements // check user schema has all required elements
if (table._id === ViewNames.USERS) { if (table._id === InternalTables.USER_METADATA) {
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) { for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
// check if the schema exists on the table to be created/updated // check if the schema exists on the table to be created/updated
if (table.schema[key] == null) { if (table.schema[key] == null) {

View File

@ -1,71 +1,23 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { const {
generateUserID, generateUserMetadataID,
getUserParams, getUserMetadataParams,
getEmailFromUserID, getEmailFromUserMetadataID,
} = require("@budibase/auth") } = require("../../db/utils")
const { InternalTables } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
const { getRole } = require("../../utilities/security/roles") const { getRole } = require("../../utilities/security/roles")
const { checkSlashesInUrl } = require("../../utilities") const {
const env = require("../../environment") getGlobalUsers,
const fetch = require("node-fetch") saveGlobalUser,
deleteGlobalUser,
async function deleteGlobalUser(email) { } = require("../../utilities/workerRequests")
const endpoint = `/api/admin/users/${email}`
const reqCfg = { method: "DELETE" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
return response.json()
}
async function getGlobalUsers(email = null) {
const endpoint = email ? `/api/admin/users/${email}` : `/api/admin/users`
const reqCfg = { method: "GET" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
return response.json()
}
async function saveGlobalUser(appId, email, body) {
const globalUser = await getGlobalUsers(email)
const roles = globalUser.roles || {}
if (body.roleId) {
roles.appId = body.roleId
}
const endpoint = `/api/admin/users`
const reqCfg = {
method: "POST",
body: {
...globalUser,
email,
password: body.password,
status: body.status,
roles,
},
}
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
reqCfg
)
await response.json()
delete body.email
delete body.password
delete body.roleId
delete body.status
return body
}
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() const global = await getGlobalUsers(ctx, ctx.appId)
const metadata = ( const metadata = (
await database.allDocs( await database.allDocs(
getUserParams(null, { getUserMetadataParams(null, {
include_docs: true, include_docs: true,
}) })
) )
@ -76,6 +28,8 @@ exports.fetchMetadata = async function(ctx) {
users.push({ users.push({
...user, ...user,
...info, ...info,
// make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(user.email),
}) })
} }
ctx.body = users ctx.body = users
@ -90,17 +44,20 @@ exports.createMetadata = async function(ctx) {
const role = await getRole(appId, roleId) const role = await getRole(appId, roleId)
if (!role) ctx.throw(400, "Invalid Role") if (!role) ctx.throw(400, "Invalid Role")
const metadata = await saveGlobalUser(appId, email, ctx.request.body) const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body)
const user = { const user = {
...metadata, ...metadata,
_id: generateUserID(email), _id: generateUserMetadataID(email),
type: "user", type: "user",
tableId: InternalTables.USER_METADATA, tableId: InternalTables.USER_METADATA,
} }
const response = await db.post(user) const response = await db.post(user)
// for automations to make it obvious was successful
ctx.status = 200
ctx.body = { ctx.body = {
_id: response.id,
_rev: response.rev, _rev: response.rev,
email, email,
} }
@ -110,11 +67,11 @@ exports.updateMetadata = async function(ctx) {
const appId = ctx.appId const appId = ctx.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
const user = ctx.request.body const user = ctx.request.body
let email = user.email || getEmailFromUserID(user._id) let email = user.email || getEmailFromUserMetadataID(user._id)
const metadata = await saveGlobalUser(appId, email, ctx.request.body) const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body)
if (!metadata._id) { if (!metadata._id) {
user._id = generateUserID(email) user._id = generateUserMetadataID(email)
} }
ctx.body = await db.put({ ctx.body = await db.put({
...metadata, ...metadata,
@ -123,9 +80,15 @@ exports.updateMetadata = async function(ctx) {
exports.destroyMetadata = async function(ctx) { exports.destroyMetadata = async function(ctx) {
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
const email = ctx.params.email const email =
await deleteGlobalUser(email) ctx.params.email || getEmailFromUserMetadataID(ctx.params.userId)
await db.destroy(generateUserID(email)) await deleteGlobalUser(ctx, email)
try {
const dbUser = await db.get(generateUserMetadataID(email))
await db.remove(dbUser._id, dbUser._rev)
} catch (err) {
// error just means the global user has no config in this app
}
ctx.body = { ctx.body = {
message: `User ${ctx.params.email} deleted.`, message: `User ${ctx.params.email} deleted.`,
} }
@ -133,12 +96,14 @@ exports.destroyMetadata = async function(ctx) {
exports.findMetadata = async function(ctx) { exports.findMetadata = async function(ctx) {
const database = new CouchDB(ctx.appId) const database = new CouchDB(ctx.appId)
let lookup = ctx.params.email const email =
? generateUserID(ctx.params.email) ctx.params.email || getEmailFromUserMetadataID(ctx.params.userId)
: ctx.params.userId const global = await getGlobalUsers(ctx, ctx.appId, email)
const user = await database.get(lookup) const user = await database.get(generateUserMetadataID(email))
if (user) { ctx.body = {
delete user.password ...global,
...user,
// make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(email),
} }
ctx.body = user
} }

View File

@ -75,7 +75,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
request: { request: {
body: inputs.row, body: inputs.row,
}, },
user: { appId }, appId,
eventEmitter: emitter, eventEmitter: emitter,
} }

View File

@ -62,9 +62,7 @@ module.exports.definition = {
module.exports.run = async function({ inputs, appId, apiKey, emitter }) { module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
const { email, password, roleId } = inputs const { email, password, roleId } = inputs
const ctx = { const ctx = {
user: { appId,
appId: appId,
},
request: { request: {
body: { email, password, roleId }, body: { email, password, roleId },
}, },
@ -79,7 +77,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
return { return {
response: ctx.body, response: ctx.body,
// internal property not returned through the API // internal property not returned through the API
id: ctx.userId, id: ctx.body._id,
revision: ctx.body._rev, revision: ctx.body._rev,
success: ctx.status === 200, success: ctx.status === 200,
} }

View File

@ -65,7 +65,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
rowId: inputs.id, rowId: inputs.id,
revId: inputs.revision, revId: inputs.revision,
}, },
user: { appId }, appId,
eventEmitter: emitter, eventEmitter: emitter,
} }

View File

@ -78,7 +78,7 @@ module.exports.run = async function({ inputs, appId, emitter }) {
request: { request: {
body: inputs.row, body: inputs.row,
}, },
user: { appId }, appId,
eventEmitter: emitter, eventEmitter: emitter,
} }

View File

@ -1,10 +1,10 @@
require("../../environment")
const automation = require("../index") const automation = require("../index")
const usageQuota = require("../../utilities/usageQuota") const usageQuota = require("../../utilities/usageQuota")
const thread = require("../thread") const thread = require("../thread")
const triggers = require("../triggers") const triggers = require("../triggers")
const { basicAutomation, basicTable } = require("../../tests/utilities/structures") const { basicAutomation, basicTable } = require("../../tests/utilities/structures")
const { wait } = require("../../utilities") const { wait } = require("../../utilities")
const env = require("../../environment")
const { makePartial } = require("../../tests/utilities") const { makePartial } = require("../../tests/utilities")
const { cleanInputValues } = require("../automationUtils") const { cleanInputValues } = require("../automationUtils")
const setup = require("./utilities") const setup = require("./utilities")

View File

@ -26,6 +26,7 @@ describe("test the create row action", () => {
}) })
expect(res.id).toBeDefined() expect(res.id).toBeDefined()
expect(res.revision).toBeDefined() expect(res.revision).toBeDefined()
expect(res.success).toEqual(true)
const gottenRow = await config.getRow(table._id, res.id) const gottenRow = await config.getRow(table._id, res.id)
expect(gottenRow.name).toEqual("test") expect(gottenRow.name).toEqual("test")
expect(gottenRow.description).toEqual("test") expect(gottenRow.description).toEqual("test")

View File

@ -1,8 +1,7 @@
const usageQuota = require("../../utilities/usageQuota") const usageQuota = require("../../utilities/usageQuota")
const env = require("../../environment")
const setup = require("./utilities") const setup = require("./utilities")
const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
const { ViewNames } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
jest.mock("../../utilities/usageQuota") jest.mock("../../utilities/usageQuota")
@ -25,8 +24,7 @@ describe("test the create user action", () => {
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user) const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user)
expect(res.id).toBeDefined() expect(res.id).toBeDefined()
expect(res.revision).toBeDefined() expect(res.revision).toBeDefined()
const userDoc = await config.getRow(ViewNames.USERS, res.id) const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id)
expect(userDoc.email).toEqual(user.email)
}) })
it("should return an error if no inputs provided", async () => { it("should return an error if no inputs provided", async () => {

View File

@ -116,17 +116,19 @@ exports.getRowParams = (tableId = null, rowId = null, otherProps = {}) => {
/** /**
* Gets a new row ID for the specified table. * Gets a new row ID for the specified table.
* @param {string} tableId The table which the row is being created for. * @param {string} tableId The table which the row is being created for.
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
* @returns {string} The new ID which a row doc can be stored under. * @returns {string} The new ID which a row doc can be stored under.
*/ */
exports.generateRowID = tableId => { exports.generateRowID = (tableId, id = null) => {
return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${newid()}` id = id || newid()
return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}`
} }
/** /**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. * Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/ */
exports.getUserParams = (email = "", otherProps = {}) => { exports.getUserMetadataParams = (email = "", otherProps = {}) => {
return exports.getRowParams(ViewNames.USERS, email, otherProps) return exports.getRowParams(InternalTables.USER_METADATA, email, otherProps)
} }
/** /**
@ -134,8 +136,17 @@ exports.getUserParams = (email = "", otherProps = {}) => {
* @param {string} email The email which the ID is going to be built up of. * @param {string} email The email which the ID is going to be built up of.
* @returns {string} The new user ID which the user doc can be stored under. * @returns {string} The new user ID which the user doc can be stored under.
*/ */
exports.generateUserID = email => { exports.generateUserMetadataID = email => {
return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}` return exports.generateRowID(InternalTables.USER_METADATA, email)
}
/**
* Breaks up the ID to get the email address back out of it.
*/
exports.getEmailFromUserMetadataID = id => {
return id.split(
`${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
)[1]
} }
/** /**

View File

@ -20,9 +20,7 @@ class TestConfiguration {
this.ctx = { this.ctx = {
throw: this.throw, throw: this.throw,
next: this.next, next: this.next,
user: { appId: "test",
appId: "test"
},
request: { request: {
body: {} body: {}
}, },

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../db")
const { StaticDatabases } = require("../../db/utils") const { StaticDatabases } = require("../../db/utils")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const env = require("../../environment") const env = require("../../environment")
const { getDeployedApps } = require("../../utilities/workerRequests")
const PROD_HOSTING_URL = "app.budi.live" const PROD_HOSTING_URL = "app.budi.live"
@ -84,30 +85,4 @@ exports.getTemplatesUrl = async (appId, type, name) => {
return `${protocol}${hostingInfo.templatesUrl}/${path}` return `${protocol}${hostingInfo.templatesUrl}/${path}`
} }
exports.getDeployedApps = async () => { exports.getDeployedApps = getDeployedApps
if (!env.SELF_HOSTED) {
throw "Can only check apps for self hosted environments"
}
const workerUrl = env.WORKER_URL
const hostingKey = env.HOSTING_KEY
try {
const response = await fetch(`${workerUrl}/api/apps`, {
method: "GET",
headers: {
"x-budibase-auth": hostingKey,
},
})
const json = await response.json()
const apps = {}
for (let [key, value] of Object.entries(json)) {
if (value.url) {
value.url = value.url.toLowerCase()
apps[key] = value
}
}
return apps
} catch (err) {
// error, cannot determine deployed apps, don't stop app creation - sort this later
return {}
}
}

View File

@ -0,0 +1,118 @@
const fetch = require("node-fetch")
const env = require("../environment")
const { checkSlashesInUrl } = require("./index")
const { BUILTIN_ROLE_IDS } = require("./security/roles")
function getAppRole(appId, user) {
if (!user.roles) {
return user
}
user.roleId = user.roles[appId]
if (!user.roleId) {
user.roleId = BUILTIN_ROLE_IDS.PUBLIC
}
delete user.roles
return user
}
function prepRequest(ctx, request) {
if (!request.headers) {
request.headers = {}
}
if (request.body) {
request.headers["Content-Type"] = "application/json"
request.body =
typeof request.body === "object"
? JSON.stringify(request.body)
: request.body
}
if (ctx.headers) {
request.headers.cookie = ctx.headers.cookie
}
return request
}
exports.getDeployedApps = async ctx => {
if (!env.SELF_HOSTED) {
throw "Can only check apps for self hosted environments"
}
try {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/apps`),
prepRequest(ctx, {
method: "GET",
})
)
const json = await response.json()
const apps = {}
for (let [key, value] of Object.entries(json)) {
if (value.url) {
value.url = value.url.toLowerCase()
apps[key] = value
}
}
return apps
} catch (err) {
// error, cannot determine deployed apps, don't stop app creation - sort this later
return {}
}
}
exports.deleteGlobalUser = async (ctx, email) => {
const endpoint = `/api/admin/users/${email}`
const reqCfg = { method: "DELETE" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
prepRequest(ctx, reqCfg)
)
return response.json()
}
exports.getGlobalUsers = async (ctx, appId, email = null) => {
const endpoint = email ? `/api/admin/users/${email}` : `/api/admin/users`
const reqCfg = { method: "GET" }
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
prepRequest(ctx, reqCfg)
)
let users = await response.json()
if (Array.isArray(users)) {
users = users.map(user => getAppRole(appId, user))
} else {
users = getAppRole(appId, users)
}
return users
}
exports.saveGlobalUser = async (ctx, appId, email, body) => {
const globalUser = await exports.getGlobalUsers(ctx, appId, email)
const roles = globalUser.roles || {}
if (body.roleId) {
roles[appId] = body.roleId
}
const endpoint = `/api/admin/users`
const reqCfg = {
method: "POST",
body: {
...globalUser,
email,
password: body.password,
status: body.status,
roles,
},
}
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
prepRequest(ctx, reqCfg)
)
const json = await response.json()
if (json.status !== 200 && response.status !== 200) {
ctx.throw(400, "Unable to save global user.")
}
delete body.email
delete body.password
delete body.roleId
delete body.status
return body
}

View File

@ -14,11 +14,11 @@ exports.userSave = async ctx => {
const { email, password, _id } = ctx.request.body const { email, password, _id } = ctx.request.body
const hashedPassword = password ? await hash(password) : null const hashedPassword = password ? await hash(password) : null
let user = { let user = {
...ctx.request.body, ...ctx.request.body,
_id: generateUserID(email), _id: generateUserID(email),
password: hashedPassword, password: hashedPassword,
}, }
dbUser let dbUser
// in-case user existed already // in-case user existed already
if (_id) { if (_id) {
dbUser = await db.get(_id) dbUser = await db.get(_id)
@ -48,7 +48,8 @@ exports.userSave = async ctx => {
exports.userDelete = async ctx => { exports.userDelete = async ctx => {
const db = new CouchDB(USER_DB) const db = new CouchDB(USER_DB)
await db.destroy(generateUserID(ctx.params.email)) const dbUser = await db.get(generateUserID(ctx.params.email))
await db.remove(dbUser._id, dbUser._rev)
ctx.body = { ctx.body = {
message: `User ${ctx.params.email} deleted.`, message: `User ${ctx.params.email} deleted.`,
} }
@ -57,13 +58,12 @@ exports.userDelete = async ctx => {
// called internally by app server user fetch // called internally by app server user fetch
exports.userFetch = async ctx => { exports.userFetch = async ctx => {
const db = new CouchDB(USER_DB) const db = new CouchDB(USER_DB)
const users = ( const response = await db.allDocs(
await db.allDocs( getUserParams(null, {
getUserParams(null, { include_docs: true,
include_docs: true, })
}) )
) const users = response.rows.map(row => row.doc)
).rows.map(row => row.doc)
// 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) {
@ -76,7 +76,13 @@ exports.userFetch = async ctx => {
// called internally by app server user find // called internally by app server user find
exports.userFind = async ctx => { exports.userFind = async ctx => {
const db = new CouchDB(USER_DB) const db = new CouchDB(USER_DB)
const user = await db.get(generateUserID(ctx.params.email)) let user
try {
user = await db.get(generateUserID(ctx.params.email))
} catch (err) {
// no user found, just return nothing
user = {}
}
if (user) { if (user) {
delete user.password delete user.password
} }

View File

@ -15,9 +15,14 @@ exports.getApps = async ctx => {
} }
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX)) const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const appPromises = appDbNames.map(db => new CouchDB(db).get(db)) const appPromises = appDbNames.map(db => new CouchDB(db).get(db))
const apps = await Promise.all(appPromises)
const apps = await Promise.allSettled(appPromises)
const body = {} const body = {}
for (let app of apps) { for (let app of apps) {
if (app.status !== "fulfilled") {
continue
}
app = app.value
let url = app.url || encodeURI(`${app.name}`) let url = app.url || encodeURI(`${app.name}`)
url = `/${url.replace(URL_REGEX_SLASH, "")}` url = `/${url.replace(URL_REGEX_SLASH, "")}`
body[url] = { body[url] = {

View File

@ -1,9 +1,9 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/app") const controller = require("../controllers/app")
const checkKey = require("../../middleware/check-key") const authenticated = require("../../middleware/authenticated")
const router = Router() const router = Router()
router.get("/api/apps", checkKey, controller.getApps) router.get("/api/apps", authenticated, controller.getApps)
module.exports = router module.exports = router