diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts new file mode 100644 index 0000000000..7197e4d6b0 --- /dev/null +++ b/packages/backend-core/src/events/publishers/group.ts @@ -0,0 +1,64 @@ +import { publishEvent } from "../events" +import { + Event, + UserGroup, + GroupCreatedEvent, + GroupDeletedEvent, + GroupUpdatedEvent, + GroupUsersAddedEvent, + GroupUsersDeletedEvent, + GroupsAddedOnboarding, + UserGroupRoles, +} from "@budibase/types" + +export async function created(group: UserGroup, timestamp?: number) { + const properties: GroupCreatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) +} + +export async function updated(group: UserGroup) { + const properties: GroupUpdatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_UPDATED, properties) +} + +export async function deleted(group: UserGroup) { + const properties: GroupDeletedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_DELETED, properties) +} + +export async function usersAdded(emails: string[], group: UserGroup) { + const properties: GroupUsersAddedEvent = { + emails, + count: emails.length, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USER_ADDED, properties) +} + +export async function usersDeleted(emails: string[], group: UserGroup) { + const properties: GroupUsersDeletedEvent = { + emails, + count: emails.length, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USER_REMOVED, properties) +} + +export async function createdOnboarding(groupId: string) { + const properties: GroupsAddedOnboarding = { + groupId: groupId, + onboarding: true, + } + await publishEvent(Event.USER_GROUP_ONBOARDING, properties) +} + +export async function permissionsEdited(roles: UserGroupRoles) { + const properties: UserGroupRoles = roles + await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties) +} diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 65785d4d8b..57fd0bf8e2 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -17,3 +17,4 @@ export * as user from "./user" export * as view from "./view" export * as installation from "./installation" export * as backfill from "./backfill" +export * as group from "./group" diff --git a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte index fde2c7213e..9e8efeee4b 100644 --- a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte @@ -53,7 +53,7 @@ let user = await users.get(id) - let userGroups = user.groups || [] + let userGroups = user.userGroups || [] userGroups.push(groupId) await users.save({ ...user, diff --git a/packages/types/src/documents/global/index.ts b/packages/types/src/documents/global/index.ts index da01c73304..77785c72e4 100644 --- a/packages/types/src/documents/global/index.ts +++ b/packages/types/src/documents/global/index.ts @@ -1,2 +1,3 @@ export * from "./config" export * from "./user" +export * from "./userGroup" diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 2de9ff7719..fe0b6b46d7 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -13,6 +13,7 @@ export interface User extends Document { providerType?: string password?: string status?: string + createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() } diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts new file mode 100644 index 0000000000..eda0cdfeca --- /dev/null +++ b/packages/types/src/documents/global/userGroup.ts @@ -0,0 +1,15 @@ +import { Document } from "../document" +import { User } from "./user" +export interface UserGroup extends Document { + name: string + icon: string + color: string + users: User[] + apps: any + roles: UserGroupRoles + createdAt?: number +} + +export interface UserGroupRoles { + [key: string]: string +} diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 5dc1707fa7..2d63addc0e 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -150,6 +150,15 @@ export enum Event { TENANT_BACKFILL_FAILED = "tenant:backfill:failed", INSTALLATION_BACKFILL_SUCCEEDED = "installation:backfill:succeeded", INSTALLATION_BACKFILL_FAILED = "installation:backfill:failed", + + // USER + USER_GROUP_CREATED = "user_group:created", + USER_GROUP_UPDATED = "user_group:updated", + USER_GROUP_DELETED = "user_group:deleted", + USER_GROUP_USER_ADDED = "user_group_user:added", + USER_GROUP_USER_REMOVED = "user_group_user:deleted", + USER_GROUP_PERMISSIONS_EDITED = "user_group_permissions:edited", + USER_GROUP_ONBOARDING = "user_group_onboarding:added", } // properties added at the final stage of the event pipeline diff --git a/packages/types/src/sdk/events/index.ts b/packages/types/src/sdk/events/index.ts index 5822f66597..88d5a12e6e 100644 --- a/packages/types/src/sdk/events/index.ts +++ b/packages/types/src/sdk/events/index.ts @@ -18,3 +18,4 @@ export * from "./view" export * from "./account" export * from "./backfill" export * from "./identification" +export * from "./userGroup" diff --git a/packages/types/src/sdk/events/userGroup.ts b/packages/types/src/sdk/events/userGroup.ts new file mode 100644 index 0000000000..f283c91c80 --- /dev/null +++ b/packages/types/src/sdk/events/userGroup.ts @@ -0,0 +1,24 @@ +import { BaseEvent } from "./event" + +export interface GroupCreatedEvent extends BaseEvent { + groupId: string +} + +export interface GroupUpdatedEvent extends BaseEvent { + groupId: string +} + +export interface GroupDeletedEvent extends BaseEvent { + groupId: string +} + +export interface GroupUsersAddedEvent extends BaseEvent { + emails: string[] + count: number + groupId: string +} + +export interface GroupsAddedOnboarding extends BaseEvent { + groupId: string + onboarding: boolean +} diff --git a/packages/worker/src/api/controllers/global/groups.ts b/packages/worker/src/api/controllers/global/groups.ts index 3851456063..62c99e80b0 100644 --- a/packages/worker/src/api/controllers/global/groups.ts +++ b/packages/worker/src/api/controllers/global/groups.ts @@ -1,24 +1,64 @@ -const { Configs } = require("../../../constants") -const email = require("../../../utilities/email") -const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy") -const env = require("../../../environment") -const { - withCache, - CacheKeys, - bustCache, -} = require("@budibase/backend-core/cache") +import { UserGroup } from "@budibase/types" +const { difference } = require("lodash/fp") + +const { events } = require("@budibase/backend-core") +const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { groups } = require("@budibase/pro") exports.save = async function (ctx: any) { const db = getGlobalDB() + let group: UserGroup = ctx.request.body + const oldGroup: UserGroup = await db.get(group._id) + let eventFns = [] // Config does not exist yet - if (!ctx.request.body._id) { - ctx.request.body._id = groups.generateUserGroupID(ctx.request.body.name) + if (!group._id) { + group._id = groups.generateUserGroupID(ctx.request.body.name) + eventFns.push(() => events.group.created(group)) + } else { + // Get the diff between the old users and new users for + // event processing purposes + let uniqueOld = group.users.filter(g => { + return !oldGroup.users.some(og => { + return g._id == og._id + }) + }) + + let uniqueNew = oldGroup.users.filter(g => { + return !group.users.some(og => { + return g._id == og._id + }) + }) + let newUsers = uniqueOld.concat(uniqueNew) + + eventFns.push(() => events.group.updated(group)) + + if (group.users.length < oldGroup.users.length) { + eventFns.push(() => + events.group.usersDeleted( + newUsers.map(u => u.email), + group + ) + ) + } else if (group.users.length > oldGroup.users.length) { + eventFns.push(() => + events.group.usersAdded( + newUsers.map(u => u.email), + group + ) + ) + } + + if (JSON.stringify(oldGroup.roles) !== JSON.stringify(group.roles)) { + eventFns.push(() => events.group.permissionsEdited(group.roles)) + } } try { - const response = await db.put(ctx.request.body) + for (const fn of eventFns) { + await fn() + } + const response = await db.put(group) ctx.body = { _id: response.id, _rev: response.rev, @@ -41,9 +81,10 @@ exports.fetch = async function (ctx: any) { exports.destroy = async function (ctx: any) { const db = getGlobalDB() const { id, rev } = ctx.params - + const group = await db.get(id) try { await db.remove(id, rev) + await events.group.deleted(group) ctx.body = { message: "Group deleted successfully" } } catch (err: any) { ctx.throw(err.status, err) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 304584dea2..3bc733b866 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -3,7 +3,7 @@ import { checkInviteCode } from "../../../utilities/redis" import { sendEmail } from "../../../utilities/email" import { users } from "../../../sdk" import env from "../../../environment" -import { User, CloudAccount } from "@budibase/types" +import { User, CloudAccount, UserGroup } from "@budibase/types" import { events, errors, @@ -24,53 +24,54 @@ export const save = async (ctx: any) => { export const bulkSave = async (ctx: any) => { let { users: newUsersRequested, groups } = ctx.request.body - let usersToSave: any[] = [] let groupsToSave: any[] = [] const newUsers: any[] = [] const db = tenancy.getGlobalDB() const currentUserEmails = (await users.allUsers())?.map((x: any) => x.email) || [] - for (const newUser of newUsersRequested) { if ( newUsers.find((x: any) => x.email === newUser.email) || currentUserEmails.includes(newUser.email) ) continue - + newUser.userGroups = groups newUsers.push(newUser) } - newUsers.forEach((user: any) => { - usersToSave.push( - users.save(user, { + if (groups.length) { + groups.forEach(async (groupId: string) => { + let oldGroup = await db.get(groupId) + groupsToSave.push(oldGroup) + }) + } + + try { + let response = [] + for (const user of newUsers) { + response = await users.save(user, { hashPassword: true, requirePassword: user.requirePassword, - bulkCreate: true, - }) - ) - - if (groups.length) { - groups.forEach(async (groupId: string) => { - let oldGroup = await db.get(groupId) - groupsToSave.push(oldGroup) + bulkCreate: false, }) } - }) - try { - const allUsers = await Promise.all(usersToSave) - let response = await db.bulkDocs(allUsers) // delete passwords and add to group - allUsers.forEach(user => { + newUsers.forEach(user => { delete user.password }) - if (groupsToSave.length) - groupsToSave.forEach(async group => { - group.users = [...group.users, ...allUsers] - await db.put(group) + if (groupsToSave.length) { + groupsToSave.forEach(async (userGroup: UserGroup) => { + userGroup.users = [...userGroup.users, ...newUsers] + await db.put(userGroup) + events.group.usersAdded( + newUsers.map(u => u.email), + userGroup + ) + events.group.createdOnboarding(userGroup._id as string) }) + } ctx.body = response } catch (err: any) { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 73a80ae086..35a4083df9 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -31,9 +31,8 @@ export const allUsers = async () => { export const paginatedUsers = async ({ page, - email, - appId, -}: { page?: string; email?: string; appId?: string } = {}) => { + search, +}: { page?: string; search?: string } = {}) => { const db = tenancy.getGlobalDB() // get one extra document, to have the next page const opts: any = { @@ -45,24 +44,19 @@ export const paginatedUsers = async ({ opts.startkey = page } // property specifies what to use for the page/anchor - let userList, - property = "_id", - getKey - if (appId) { - userList = await usersCore.searchGlobalUsersByApp(appId, opts) - getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc) - } else if (email) { - userList = await usersCore.searchGlobalUsersByEmail(email, opts) - property = "email" - } else { - // no search, query allDocs + let userList, property + // no search, query allDocs + if (!search) { const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts)) userList = response.rows.map((row: any) => row.doc) + property = "_id" + } else { + userList = await usersCore.searchGlobalUsersByEmail(search, opts) + property = "email" } return dbUtils.pagination(userList, PAGE_LIMIT, { paginate: true, property, - getKey, }) }