diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index d1fe7f7251..6972a2e613 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -15,7 +15,7 @@ export const buildUserEndpoints = API => ({ */ fetchBuilderSelf: async () => { return await API.get({ - url: "/api/global/users/self", + url: "/api/global/self", }) }, @@ -67,7 +67,7 @@ export const buildUserEndpoints = API => ({ */ updateSelf: async user => { return await API.post({ - url: "/api/global/users/self", + url: "/api/global/self", body: user, }) }, diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 91db63d2a4..5c333a48ca 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -59,7 +59,7 @@ exports.sendSmtpEmail = async (to, from, subject, contents, automation) => { } exports.getGlobalSelf = async (ctx, appId = null) => { - const endpoint = `/api/global/users/self` + const endpoint = `/api/global/self` const response = await fetch( checkSlashesInUrl(env.WORKER_URL + endpoint), // we don't want to use API key when getting self diff --git a/packages/worker/src/api/controllers/global/self.js b/packages/worker/src/api/controllers/global/self.js index 4c2fa961b9..26e07e63bb 100644 --- a/packages/worker/src/api/controllers/global/self.js +++ b/packages/worker/src/api/controllers/global/self.js @@ -1,9 +1,12 @@ const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy") const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db") +const { user: userCache } = require("@budibase/backend-core/cache") +const { hash, platformLogout } = require("@budibase/backend-core/utils") const { newid } = require("@budibase/backend-core/utils") +const { getUser } = require("../../utilities") function newApiKey() { - return `${getTenantId()}${SEPARATOR}${newid()}` + return hash(`${getTenantId()}${SEPARATOR}${newid()}`) } function cleanupDevInfo(info) { @@ -43,3 +46,49 @@ exports.fetchAPIKey = async ctx => { } ctx.body = cleanupDevInfo(devInfo) } + +exports.getSelf = async ctx => { + if (!ctx.user) { + ctx.throw(403, "User not logged in") + } + const userId = ctx.user._id + ctx.params = { + id: userId, + } + // get the main body of the user + ctx.body = await getUser(userId) + // forward session information not found in db + ctx.body.account = ctx.user.account + ctx.body.budibaseAccess = ctx.user.budibaseAccess + ctx.body.accountPortalAccess = ctx.user.accountPortalAccess + ctx.body.csrfToken = ctx.user.csrfToken +} + +exports.updateSelf = async ctx => { + const db = getGlobalDB() + const user = await db.get(ctx.user._id) + if (ctx.request.body.password) { + // changing password + ctx.request.body.password = await hash(ctx.request.body.password) + // Log all other sessions out apart from the current one + await platformLogout({ + ctx, + userId: ctx.user._id, + keepActiveSession: true, + }) + } + // don't allow sending up an ID/Rev, always use the existing one + delete ctx.request.body._id + delete ctx.request.body._rev + // don't allow setting the csrf token + delete ctx.request.body.csrfToken + const response = await db.put({ + ...user, + ...ctx.request.body, + }) + await userCache.invalidateUser(user._id) + ctx.body = { + _id: response.id, + _rev: response.rev, + } +} diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index 38316396b7..f9b8552b38 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -4,10 +4,8 @@ const { generateNewUsageQuotaDoc, } = require("@budibase/backend-core/db") const { - hash, getGlobalUserByEmail, saveUser, - platformLogout, } = require("@budibase/backend-core/utils") const { EmailTemplatePurpose } = require("../../../constants") const { checkInviteCode } = require("../../../utilities/redis") @@ -24,7 +22,7 @@ const { const { removeUserFromInfoDB } = require("@budibase/backend-core/deprovision") const env = require("../../../environment") const { syncUserInApps } = require("../../../utilities/appService") -const { allUsers } = require("../../utilities") +const { allUsers, getUser } = require("../../utilities") exports.save = async ctx => { try { @@ -129,52 +127,6 @@ exports.destroy = async ctx => { } } -exports.getSelf = async ctx => { - if (!ctx.user) { - ctx.throw(403, "User not logged in") - } - ctx.params = { - id: ctx.user._id, - } - // this will set the body - await exports.find(ctx) - - // forward session information not found in db - ctx.body.account = ctx.user.account - ctx.body.budibaseAccess = ctx.user.budibaseAccess - ctx.body.accountPortalAccess = ctx.user.accountPortalAccess - ctx.body.csrfToken = ctx.user.csrfToken -} - -exports.updateSelf = async ctx => { - const db = getGlobalDB() - const user = await db.get(ctx.user._id) - if (ctx.request.body.password) { - // changing password - ctx.request.body.password = await hash(ctx.request.body.password) - // Log all other sessions out apart from the current one - await platformLogout({ - ctx, - userId: ctx.user._id, - keepActiveSession: true, - }) - } - // don't allow sending up an ID/Rev, always use the existing one - delete ctx.request.body._id - delete ctx.request.body._rev - // don't allow setting the csrf token - delete ctx.request.body.csrfToken - const response = await db.put({ - ...user, - ...ctx.request.body, - }) - await userCache.invalidateUser(user._id) - ctx.body = { - _id: response.id, - _rev: response.rev, - } -} - // called internally by app server user fetch exports.fetch = async ctx => { const users = await allUsers(ctx) @@ -189,18 +141,7 @@ exports.fetch = async ctx => { // called internally by app server user find exports.find = async ctx => { - const db = getGlobalDB() - let user - try { - user = await db.get(ctx.params.id) - } catch (err) { - // no user found, just return nothing - user = {} - } - if (user) { - delete user.password - } - ctx.body = user + ctx.body = await getUser(ctx.params.id) } exports.tenantUserLookup = async ctx => { diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index 607d8283f9..76e45991ff 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -67,6 +67,10 @@ const NO_TENANCY_ENDPOINTS = [ route: "/api/global/users/self", method: "GET", }, + { + route: "/api/global/self", + method: "GET", + }, ] // most public endpoints are gets, but some are posts diff --git a/packages/worker/src/api/routes/global/self.js b/packages/worker/src/api/routes/global/self.js index d463def1f3..eae16857a8 100644 --- a/packages/worker/src/api/routes/global/self.js +++ b/packages/worker/src/api/routes/global/self.js @@ -1,11 +1,18 @@ const Router = require("@koa/router") const controller = require("../../controllers/global/self") const builderOnly = require("../../../middleware/builderOnly") +const { buildUserSaveValidation } = require("../../utilities/validation") const router = Router() router .post("/api/global/self/api_key", builderOnly, controller.generateAPIKey) .get("/api/global/self/api_key", builderOnly, controller.fetchAPIKey) + .get("/api/global/self", controller.getSelf) + .post( + "/api/global/self", + buildUserSaveValidation(true), + controller.updateSelf + ) module.exports = router diff --git a/packages/worker/src/api/routes/global/users.js b/packages/worker/src/api/routes/global/users.js index 31d6658ec5..5d28d18eb7 100644 --- a/packages/worker/src/api/routes/global/users.js +++ b/packages/worker/src/api/routes/global/users.js @@ -4,6 +4,8 @@ const joiValidator = require("../../../middleware/joi-validator") const adminOnly = require("../../../middleware/adminOnly") const Joi = require("joi") const cloudRestricted = require("../../../middleware/cloudRestricted") +const { buildUserSaveValidation } = require("../../utilities/validation") +const selfController = require("../../controllers/global/self") const router = Router() @@ -19,32 +21,6 @@ function buildAdminInitValidation() { ) } -function buildUserSaveValidation(isSelf = false) { - let schema = { - email: Joi.string().allow(null, ""), - password: Joi.string().allow(null, ""), - forceResetPassword: Joi.boolean().optional(), - firstName: Joi.string().allow(null, ""), - lastName: Joi.string().allow(null, ""), - builder: Joi.object({ - global: Joi.boolean().optional(), - apps: Joi.array().optional(), - }) - .unknown(true) - .optional(), - // maps appId -> roleId for the user - roles: Joi.object().pattern(/.*/, Joi.string()).required().unknown(true), - } - if (!isSelf) { - schema = { - ...schema, - _id: Joi.string(), - _rev: Joi.string(), - } - } - return joiValidator.body(Joi.object(schema).required().unknown(true)) -} - function buildInviteValidation() { // prettier-ignore return joiValidator.body(Joi.object({ @@ -78,11 +54,6 @@ router controller.invite ) // non-global endpoints - .post( - "/api/global/users/self", - buildUserSaveValidation(true), - controller.updateSelf - ) .post( "/api/global/users/invite/accept", buildInviteAcceptValidation(), @@ -94,10 +65,15 @@ router buildAdminInitValidation(), controller.adminUser ) - - .get("/api/global/users/self", controller.getSelf) .get("/api/global/users/tenant/:id", controller.tenantUserLookup) // global endpoint but needs to come at end (blocks other endpoints otherwise) .get("/api/global/users/:id", adminOnly, controller.find) + // DEPRECATED - use new versions with self API + .get("/api/global/users/self", selfController.getSelf) + .post( + "/api/global/users/self", + buildUserSaveValidation(true), + selfController.updateSelf + ) module.exports = router diff --git a/packages/worker/src/api/utilities/index.js b/packages/worker/src/api/utilities/index.js index 4afe29649e..2b3605481d 100644 --- a/packages/worker/src/api/utilities/index.js +++ b/packages/worker/src/api/utilities/index.js @@ -1,6 +1,9 @@ const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalUserParams } = require("@budibase/backend-core/db") +/** + * Retrieves all users from the current tenancy. + */ exports.allUsers = async () => { const db = getGlobalDB() const response = await db.allDocs( @@ -10,3 +13,21 @@ exports.allUsers = async () => { ) return response.rows.map(row => row.doc) } + +/** + * Gets a user by ID from the global database, based on the current tenancy. + */ +exports.getUser = async userId => { + const db = getGlobalDB() + let user + try { + user = await db.get(userId) + } catch (err) { + // no user found, just return nothing + user = {} + } + if (user) { + delete user.password + } + return user +} diff --git a/packages/worker/src/api/utilities/validation.js b/packages/worker/src/api/utilities/validation.js new file mode 100644 index 0000000000..d1b36a9206 --- /dev/null +++ b/packages/worker/src/api/utilities/validation.js @@ -0,0 +1,28 @@ +const joiValidator = require("../../middleware/joi-validator") +const Joi = require("joi") + +exports.buildUserSaveValidation = (isSelf = false) => { + let schema = { + email: Joi.string().allow(null, ""), + password: Joi.string().allow(null, ""), + forceResetPassword: Joi.boolean().optional(), + firstName: Joi.string().allow(null, ""), + lastName: Joi.string().allow(null, ""), + builder: Joi.object({ + global: Joi.boolean().optional(), + apps: Joi.array().optional(), + }) + .unknown(true) + .optional(), + // maps appId -> roleId for the user + roles: Joi.object().pattern(/.*/, Joi.string()).required().unknown(true), + } + if (!isSelf) { + schema = { + ...schema, + _id: Joi.string(), + _rev: Joi.string(), + } + } + return joiValidator.body(Joi.object(schema).required().unknown(true)) +}