diff --git a/packages/server/src/api/controllers/user.ts b/packages/server/src/api/controllers/user.ts index df64ffc7d0..1ae1a68824 100644 --- a/packages/server/src/api/controllers/user.ts +++ b/packages/server/src/api/controllers/user.ts @@ -4,19 +4,21 @@ import { getGlobalUsers, getRawGlobalUser } from "../../utilities/global" import { getFullUser } from "../../utilities/users" import { context, - constants, roles as rolesCore, db as dbCore, } from "@budibase/backend-core" -import { BBContext, User } from "@budibase/types" +import { BBContext, Ctx, SyncUserRequest, User } from "@budibase/types" import sdk from "../../sdk" -export async function syncUser(ctx: BBContext) { +export async function syncUser(ctx: Ctx) { let deleting = false, user: User | any const userId = ctx.params.id + + const previousUser = ctx.request.body?.previousUser + try { - user = await getRawGlobalUser(userId) + user = (await getRawGlobalUser(userId)) as User } catch (err: any) { if (err && err.status === 404) { user = {} @@ -25,6 +27,11 @@ export async function syncUser(ctx: BBContext) { throw err } } + + let previousApps = previousUser + ? Object.keys(previousUser.roles).map(appId => appId) + : [] + const roles = deleting ? {} : user.roles // remove props which aren't useful to metadata delete user.password @@ -40,8 +47,9 @@ export async function syncUser(ctx: BBContext) { .filter(entry => entry[1] !== rolesCore.BUILTIN_ROLE_IDS.PUBLIC) .map(([appId]) => appId) } - for (let prodAppId of prodAppIds) { + for (let prodAppId of new Set([...prodAppIds, ...previousApps])) { const roleId = roles[prodAppId] + const deleteFromApp = !roleId const devAppId = dbCore.getDevelopmentAppID(prodAppId) for (let appId of [prodAppId, devAppId]) { if (!(await dbCore.dbExists(appId))) { @@ -54,24 +62,24 @@ export async function syncUser(ctx: BBContext) { try { metadata = await db.get(metadataId) } catch (err) { - if (deleting) { + if (deleteFromApp) { return } metadata = { tableId: InternalTables.USER_METADATA, } } + + if (deleteFromApp) { + await db.remove(metadata) + return + } + // assign the roleId for the metadata doc if (roleId) { metadata.roleId = roleId } - let combined = !deleting - ? sdk.users.combineMetadataAndUser(user, metadata) - : { - ...metadata, - status: constants.UserStatus.INACTIVE, - metadata: rolesCore.BUILTIN_ROLE_IDS.PUBLIC, - } + let combined = sdk.users.combineMetadataAndUser(user, metadata) // if its null then there was no updates required if (combined) { await db.put(combined) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 56f1923cb0..8f4f44d4c5 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -171,9 +171,28 @@ describe("/users", () => { .expect("Content-Type", /json/) expect(res.body.message).toEqual('User synced.') }) + + + it("should sync the user when a previous user is specified", async () => { + const app1 = await config.createApp('App 1') + const app2 = await config.createApp('App 2') + + let user = await config.createUser( + undefined, + undefined, + undefined, + undefined, + false, + true, + { [app1.appId]: 'ADMIN' }) + let res = await request + .post(`/api/users/metadata/sync/${user._id}`) + .set(config.defaultHeaders()) + .send({ previousUser: { ...user, roles: { ...user.roles, [app2.appId]: 'BASIC' } } }) + .expect(200) + .expect("Content-Type", /json/) + + expect(res.body.message).toEqual('User synced.') + }) }) - - - - }) diff --git a/packages/server/src/middleware/currentapp.ts b/packages/server/src/middleware/currentapp.ts index 2cd11aa438..593e96adcb 100644 --- a/packages/server/src/middleware/currentapp.ts +++ b/packages/server/src/middleware/currentapp.ts @@ -25,6 +25,7 @@ export default async (ctx: BBContext, next: any) => { if (!appCookie && !requestAppId) { return next() } + // check the app exists referenced in cookie if (appCookie) { const appId = appCookie.appId @@ -51,7 +52,7 @@ export default async (ctx: BBContext, next: any) => { let appId: string | undefined, roleId = roles.BUILTIN_ROLE_IDS.PUBLIC - if (!ctx.user) { + if (!ctx.user?._id) { // not logged in, try to set a cookie for public apps appId = requestAppId } else if (requestAppId != null) { @@ -96,7 +97,7 @@ export default async (ctx: BBContext, next: any) => { // need to judge this only based on the request app ID, if ( env.MULTI_TENANCY && - ctx.user && + ctx.user?._id && requestAppId && !tenancy.isUserInAppTenant(requestAppId, ctx.user) ) { diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 0ebe4ccce8..6acaf6912d 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -57,3 +57,7 @@ export interface CreateAdminUserRequest { password: string tenantId: string } + +export interface SyncUserRequest { + previousUser?: User +} diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index fbf34cb196..91543a6368 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -69,3 +69,7 @@ export interface AdminUser extends User { global: boolean } } + +export function isUser(user: object): user is User { + return !!(user as User).roles +} diff --git a/packages/types/src/sdk/koa.ts b/packages/types/src/sdk/koa.ts index 302d1aeb67..f824b73458 100644 --- a/packages/types/src/sdk/koa.ts +++ b/packages/types/src/sdk/koa.ts @@ -41,7 +41,7 @@ export interface UserCtx } /** - * Deprecated: Use UserCtx / Ctx appropriately + * @deprecated: Use UserCtx / Ctx appropriately * Authenticated context. */ export interface BBContext extends Ctx { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index f3117b63ab..5124a5c5b1 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -31,6 +31,7 @@ import { SearchUsersRequest, User, ThirdPartyUser, + isUser, } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" @@ -265,8 +266,9 @@ export const save = async ( await eventHelpers.handleSaveEvents(builtUser, dbUser) await addTenant(tenantId, _id, email) await cache.user.invalidateUser(response.id) + // let server know to sync user - await apps.syncUserInApps(_id) + await apps.syncUserInApps(_id, dbUser) await Promise.all(groupPromises) @@ -572,7 +574,7 @@ export const destroy = async (id: string, currentUser: any) => { await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "deletion" }) // let server know to sync user - await apps.syncUserInApps(userId) + await apps.syncUserInApps(userId, dbUser) } const bulkDeleteProcessing = async (dbUser: User) => { @@ -582,7 +584,7 @@ const bulkDeleteProcessing = async (dbUser: User) => { await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "bulk-deletion" }) // let server know to sync user - await apps.syncUserInApps(userId) + await apps.syncUserInApps(userId, dbUser) } export const invite = async ( diff --git a/packages/worker/src/utilities/appService.ts b/packages/worker/src/utilities/appService.ts index 244f10336f..478e986fe8 100644 --- a/packages/worker/src/utilities/appService.ts +++ b/packages/worker/src/utilities/appService.ts @@ -2,6 +2,7 @@ import fetch from "node-fetch" import { constants, tenancy, logging } from "@budibase/backend-core" import { checkSlashesInUrl } from "../utilities" import env from "../environment" +import { SyncUserRequest, User } from "@budibase/types" async function makeAppRequest(url: string, method: string, body: any) { if (env.isTest()) { @@ -24,11 +25,15 @@ async function makeAppRequest(url: string, method: string, body: any) { return fetch(checkSlashesInUrl(env.APPS_URL + url), request) } -export async function syncUserInApps(userId: string) { +export async function syncUserInApps(userId: string, previousUser?: User) { + const body: SyncUserRequest = { + previousUser, + } + const response = await makeAppRequest( `/api/users/metadata/sync/${userId}`, "POST", - {} + body ) if (response && response.status !== 200) { throw "Unable to sync user."