diff --git a/package.json b/package.json index b2f572d202..a4b0993fde 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "clean": "lerna clean", "kill-port": "kill-port 4001", "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", "lint": "eslint packages", "lint:fix": "eslint --fix packages", diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 9b6f5d7103..871423df03 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -26,13 +26,12 @@ exports.generateUserID = email => { * Gets parameters for retrieving users, this is a utility function for the getDocParams function. */ exports.getUserParams = (email = "", otherProps = {}) => { + if (!email) { + email = "" + } return { ...otherProps, startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, } } - -exports.getEmailFromUserID = id => { - return id.split(`${DocumentTypes.USER}${SEPARATOR}`)[1] -} \ No newline at end of file diff --git a/packages/builder/src/components/backend/DataTable/api.js b/packages/builder/src/components/backend/DataTable/api.js index 91ebc19b26..43c6856540 100644 --- a/packages/builder/src/components/backend/DataTable/api.js +++ b/packages/builder/src/components/backend/DataTable/api.js @@ -1,7 +1,7 @@ import api from "builderStore/api" 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) return await response.json() } @@ -15,8 +15,7 @@ export async function saveRow(row, tableId) { export async function deleteRow(row) { const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}` - const response = await api.delete(DELETE_ROWS_URL) - return response + return api.delete(DELETE_ROWS_URL) } export async function fetchDataForView(view) { diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index babe7bd0df..c727283b29 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -157,7 +157,7 @@ password: $createAppStore.values.password, 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() $goto(`./${appJson._id}`) } catch (error) { diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index cacde4bb00..042474c5b6 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -74,7 +74,7 @@ async function getAppUrlIfNotInUse(ctx) { if (!env.SELF_HOSTED) { return url } - const deployedApps = await getDeployedApps() + const deployedApps = await getDeployedApps(ctx) if ( deployedApps[url] != null && deployedApps[url].appId !== ctx.params.appId diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index da68aa485b..6e662f7b9a 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -3,7 +3,7 @@ const CouchDB = require("../../db") const bcrypt = require("../../utilities/bcrypt") const env = require("../../environment") const { getAPIKey } = require("../../utilities/usageQuota") -const { generateUserID } = require("../../db/utils") +const { generateUserMetadataID } = require("../../db/utils") const { setCookie } = require("../../utilities") const { outputProcessing } = require("../../utilities/rowProcessor") const { ViewNames } = require("../../db/utils") @@ -27,7 +27,7 @@ exports.authenticate = async ctx => { let dbUser try { - dbUser = await db.get(generateUserID(email)) + dbUser = await db.get(generateUserMetadataID(email)) } catch (_) { // do not want to throw a 404 - as this could be // used to determine valid emails diff --git a/packages/server/src/api/controllers/hosting.js b/packages/server/src/api/controllers/hosting.js index 4b070cf75b..8b7b31e00a 100644 --- a/packages/server/src/api/controllers/hosting.js +++ b/packages/server/src/api/controllers/hosting.js @@ -40,5 +40,5 @@ exports.fetchUrls = async ctx => { } exports.getDeployedApps = async ctx => { - ctx.body = await getDeployedApps() + ctx.body = await getDeployedApps(ctx) } diff --git a/packages/server/src/api/controllers/role.js b/packages/server/src/api/controllers/role.js index 11f81c1219..d27272a21a 100644 --- a/packages/server/src/api/controllers/role.js +++ b/packages/server/src/api/controllers/role.js @@ -10,7 +10,7 @@ const { const { generateRoleID, getRoleParams, - getUserParams, + getUserMetadataParams, ViewNames, } = require("../../db/utils") @@ -112,7 +112,7 @@ exports.destroy = async function(ctx) { // first check no users actively attached to role const users = ( await db.allDocs( - getUserParams(null, { + getUserMetadataParams(null, { include_docs: true, }) ) diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 1b3e795f83..8b4a461d2c 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -6,8 +6,8 @@ const { generateRowID, DocumentTypes, SEPARATOR, - ViewNames, - generateUserID, + InternalTables, + generateUserMetadataID, } = require("../../db/utils") const usersController = require("./user") const { @@ -39,7 +39,7 @@ validateJs.extend(validateJs.validators.datetime, { async function findRow(db, appId, tableId, rowId) { let row - if (tableId === ViewNames.USERS) { + if (tableId === InternalTables.USER_METADATA) { let ctx = { params: { userId: rowId, @@ -97,7 +97,7 @@ exports.patch = async function(ctx) { }) // Creation of a new user goes to the user controller - if (row.tableId === ViewNames.USERS) { + if (row.tableId === InternalTables.USER_METADATA) { // the row has been updated, need to put it into the ctx ctx.request.body = { ...row, @@ -142,8 +142,8 @@ exports.save = async function(ctx) { } if (!inputs._rev && !inputs._id) { - if (inputs.tableId === ViewNames.USERS) { - inputs._id = generateUserID(inputs.email) + if (inputs.tableId === InternalTables.USER_METADATA) { + inputs._id = generateUserMetadataID(inputs.email) } else { inputs._id = generateRowID(inputs.tableId) } @@ -176,7 +176,7 @@ exports.save = async function(ctx) { }) // Creation of a new user goes to the user controller - if (row.tableId === ViewNames.USERS) { + if (row.tableId === InternalTables.USER_METADATA) { // the row has been updated, need to put it into the ctx ctx.request.body = row await usersController.createMetadata(ctx) @@ -289,7 +289,7 @@ exports.search = async function(ctx) { const response = await search(searchString) // delete passwords from users - if (tableId === ViewNames.USERS) { + if (tableId === InternalTables.USER_METADATA) { for (let row of response.rows) { delete row.password } @@ -309,7 +309,7 @@ exports.fetchTableRows = async function(ctx) { // special case for users, fetch through the user controller let rows, table = await db.get(ctx.params.tableId) - if (ctx.params.tableId === ViewNames.USERS) { + if (ctx.params.tableId === InternalTables.USER_METADATA) { await usersController.fetchMetadata(ctx) rows = ctx.body } else { diff --git a/packages/server/src/api/controllers/static/index.js b/packages/server/src/api/controllers/static/index.js index 69a12d573b..71e20923a8 100644 --- a/packages/server/src/api/controllers/static/index.js +++ b/packages/server/src/api/controllers/static/index.js @@ -22,7 +22,7 @@ const { objectStoreUrl, clientLibraryPath } = require("../../../utilities") async function checkForSelfHostedURL(ctx) { // the "appId" component of the URL may actually be a specific self hosted URL let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}` - const apps = await getDeployedApps() + const apps = await getDeployedApps(ctx) if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) { return apps[possibleAppUrl].appId } else { diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 92b038d05e..c526050cde 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -1,71 +1,23 @@ const CouchDB = require("../../db") const { - generateUserID, - getUserParams, - getEmailFromUserID, -} = require("@budibase/auth") + generateUserMetadataID, + getUserMetadataParams, + getEmailFromUserMetadataID, +} = require("../../db/utils") const { InternalTables } = require("../../db/utils") const { getRole } = require("../../utilities/security/roles") -const { checkSlashesInUrl } = require("../../utilities") -const env = require("../../environment") -const fetch = require("node-fetch") - -async function deleteGlobalUser(email) { - 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 -} +const { + getGlobalUsers, + saveGlobalUser, + deleteGlobalUser, +} = require("../../utilities/workerRequests") exports.fetchMetadata = async function(ctx) { const database = new CouchDB(ctx.appId) - const global = await getGlobalUsers() + const global = await getGlobalUsers(ctx, ctx.appId) const metadata = ( await database.allDocs( - getUserParams(null, { + getUserMetadataParams(null, { include_docs: true, }) ) @@ -90,11 +42,11 @@ exports.createMetadata = async function(ctx) { const role = await getRole(appId, roleId) 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 = { ...metadata, - _id: generateUserID(email), + _id: generateUserMetadataID(email), type: "user", tableId: InternalTables.USER_METADATA, } @@ -110,11 +62,11 @@ exports.updateMetadata = async function(ctx) { const appId = ctx.appId const db = new CouchDB(appId) const user = ctx.request.body - let email = user.email || getEmailFromUserID(user._id) - const metadata = await saveGlobalUser(appId, email, ctx.request.body) + let email = user.email || getEmailFromUserMetadataID(user._id) + const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body) if (!metadata._id) { - user._id = generateUserID(email) + user._id = generateUserMetadataID(email) } ctx.body = await db.put({ ...metadata, @@ -124,8 +76,8 @@ exports.updateMetadata = async function(ctx) { exports.destroyMetadata = async function(ctx) { const db = new CouchDB(ctx.appId) const email = ctx.params.email - await deleteGlobalUser(email) - await db.destroy(generateUserID(email)) + await deleteGlobalUser(ctx, email) + await db.destroy(generateUserMetadataID(email)) ctx.body = { message: `User ${ctx.params.email} deleted.`, } @@ -133,12 +85,12 @@ exports.destroyMetadata = async function(ctx) { exports.findMetadata = async function(ctx) { const database = new CouchDB(ctx.appId) - let lookup = ctx.params.email - ? generateUserID(ctx.params.email) - : ctx.params.userId - const user = await database.get(lookup) - if (user) { - delete user.password + const email = + ctx.params.email || getEmailFromUserMetadataID(ctx.params.userId) + const global = await getGlobalUsers(ctx, ctx.appId, email) + const user = await database.get(generateUserMetadataID(email)) + ctx.body = { + ...global, + ...user, } - ctx.body = user } diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 8623b99f2c..63d4e30d65 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -116,16 +116,18 @@ exports.getRowParams = (tableId = null, rowId = null, otherProps = {}) => { /** * Gets a new row ID for the specified table. * @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. */ -exports.generateRowID = tableId => { - return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${newid()}` +exports.generateRowID = (tableId, id = null) => { + id = id || newid() + return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}` } /** * 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) } @@ -134,8 +136,17 @@ exports.getUserParams = (email = "", otherProps = {}) => { * @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. */ -exports.generateUserID = email => { - return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}` +exports.generateUserMetadataID = 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] } /** diff --git a/packages/server/src/utilities/builder/hosting.js b/packages/server/src/utilities/builder/hosting.js index f852cefec1..328e98ee98 100644 --- a/packages/server/src/utilities/builder/hosting.js +++ b/packages/server/src/utilities/builder/hosting.js @@ -2,6 +2,7 @@ const CouchDB = require("../../db") const { StaticDatabases } = require("../../db/utils") const fetch = require("node-fetch") const env = require("../../environment") +const { getDeployedApps } = require("../../utilities/workerRequests") const PROD_HOSTING_URL = "app.budi.live" @@ -84,30 +85,4 @@ exports.getTemplatesUrl = async (appId, type, name) => { return `${protocol}${hostingInfo.templatesUrl}/${path}` } -exports.getDeployedApps = async () => { - 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 {} - } -} +exports.getDeployedApps = getDeployedApps diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js new file mode 100644 index 0000000000..f3bf971257 --- /dev/null +++ b/packages/server/src/utilities/workerRequests.js @@ -0,0 +1,116 @@ +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 + } + 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 +} diff --git a/packages/worker/src/api/controllers/admin/index.js b/packages/worker/src/api/controllers/admin/index.js index 515feb8420..305686a3b0 100644 --- a/packages/worker/src/api/controllers/admin/index.js +++ b/packages/worker/src/api/controllers/admin/index.js @@ -14,11 +14,11 @@ exports.userSave = async ctx => { const { email, password, _id } = ctx.request.body const hashedPassword = password ? await hash(password) : null let user = { - ...ctx.request.body, - _id: generateUserID(email), - password: hashedPassword, - }, - dbUser + ...ctx.request.body, + _id: generateUserID(email), + password: hashedPassword, + } + let dbUser // in-case user existed already if (_id) { dbUser = await db.get(_id) @@ -57,13 +57,12 @@ exports.userDelete = async ctx => { // called internally by app server user fetch exports.userFetch = async ctx => { const db = new CouchDB(USER_DB) - const users = ( - await db.allDocs( - getUserParams(null, { - include_docs: true, - }) - ) - ).rows.map(row => row.doc) + const response = await db.allDocs( + getUserParams(null, { + include_docs: true, + }) + ) + const users = response.rows.map(row => row.doc) // user hashed password shouldn't ever be returned for (let user of users) { if (user) { @@ -76,7 +75,13 @@ exports.userFetch = async ctx => { // called internally by app server user find exports.userFind = async ctx => { 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) { delete user.password } diff --git a/packages/worker/src/api/routes/app.js b/packages/worker/src/api/routes/app.js index 10656e5362..75fa7164b0 100644 --- a/packages/worker/src/api/routes/app.js +++ b/packages/worker/src/api/routes/app.js @@ -1,9 +1,9 @@ const Router = require("@koa/router") const controller = require("../controllers/app") -const checkKey = require("../../middleware/check-key") +const authenticated = require("../../middleware/authenticated") const router = Router() -router.get("/api/apps", checkKey, controller.getApps) +router.get("/api/apps", authenticated, controller.getApps) module.exports = router diff --git a/packages/worker/src/middleware/authenticated.js b/packages/worker/src/middleware/authenticated.js index 751e10ee9a..b13ff5c1b7 100644 --- a/packages/worker/src/middleware/authenticated.js +++ b/packages/worker/src/middleware/authenticated.js @@ -11,20 +11,22 @@ module.exports = async (ctx, next) => { appId = cookieAppId } - return passport.authenticate("jwt", async (err, user) => { - if (err) { - return ctx.throw(err.status || 403, err) - } + return next() - try { - ctx.appId = appId - ctx.isAuthenticated = true - // TODO: introduce roles again - ctx.user = user - await next() - } catch (err) { - console.log(err) - ctx.throw(err.status || 403, err.text) - } - })(ctx, next) + // return passport.authenticate("jwt", async (err, user) => { + // if (err) { + // return ctx.throw(err.status || 403, err) + // } + // + // try { + // ctx.appId = appId + // ctx.isAuthenticated = true + // // TODO: introduce roles again + // ctx.user = user + // await next() + // } catch (err) { + // console.log(err) + // ctx.throw(err.status || 403, err.text) + // } + // })(ctx, next) }