This commit is contained in:
Peter Clement 2022-07-18 12:33:56 +01:00
parent 25a395972c
commit dfdee4d271
12 changed files with 205 additions and 53 deletions

View File

@ -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)
}

View File

@ -17,3 +17,4 @@ export * as user from "./user"
export * as view from "./view" export * as view from "./view"
export * as installation from "./installation" export * as installation from "./installation"
export * as backfill from "./backfill" export * as backfill from "./backfill"
export * as group from "./group"

View File

@ -53,7 +53,7 @@
let user = await users.get(id) let user = await users.get(id)
let userGroups = user.groups || [] let userGroups = user.userGroups || []
userGroups.push(groupId) userGroups.push(groupId)
await users.save({ await users.save({
...user, ...user,

View File

@ -1,2 +1,3 @@
export * from "./config" export * from "./config"
export * from "./user" export * from "./user"
export * from "./userGroup"

View File

@ -13,6 +13,7 @@ export interface User extends Document {
providerType?: string providerType?: string
password?: string password?: string
status?: string status?: string
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
} }

View File

@ -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
}

View File

@ -150,6 +150,15 @@ export enum Event {
TENANT_BACKFILL_FAILED = "tenant:backfill:failed", TENANT_BACKFILL_FAILED = "tenant:backfill:failed",
INSTALLATION_BACKFILL_SUCCEEDED = "installation:backfill:succeeded", INSTALLATION_BACKFILL_SUCCEEDED = "installation:backfill:succeeded",
INSTALLATION_BACKFILL_FAILED = "installation:backfill:failed", 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 // properties added at the final stage of the event pipeline

View File

@ -18,3 +18,4 @@ export * from "./view"
export * from "./account" export * from "./account"
export * from "./backfill" export * from "./backfill"
export * from "./identification" export * from "./identification"
export * from "./userGroup"

View File

@ -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
}

View File

@ -1,24 +1,64 @@
const { Configs } = require("../../../constants") import { UserGroup } from "@budibase/types"
const email = require("../../../utilities/email") const { difference } = require("lodash/fp")
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const env = require("../../../environment") const { events } = require("@budibase/backend-core")
const { const { getGlobalDB } = require("@budibase/backend-core/tenancy")
withCache,
CacheKeys,
bustCache,
} = require("@budibase/backend-core/cache")
const { groups } = require("@budibase/pro") const { groups } = require("@budibase/pro")
exports.save = async function (ctx: any) { exports.save = async function (ctx: any) {
const db = getGlobalDB() const db = getGlobalDB()
let group: UserGroup = ctx.request.body
const oldGroup: UserGroup = await db.get(group._id)
let eventFns = []
// Config does not exist yet // Config does not exist yet
if (!ctx.request.body._id) { if (!group._id) {
ctx.request.body._id = groups.generateUserGroupID(ctx.request.body.name) 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 { try {
const response = await db.put(ctx.request.body) for (const fn of eventFns) {
await fn()
}
const response = await db.put(group)
ctx.body = { ctx.body = {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
@ -41,9 +81,10 @@ exports.fetch = async function (ctx: any) {
exports.destroy = async function (ctx: any) { exports.destroy = async function (ctx: any) {
const db = getGlobalDB() const db = getGlobalDB()
const { id, rev } = ctx.params const { id, rev } = ctx.params
const group = await db.get(id)
try { try {
await db.remove(id, rev) await db.remove(id, rev)
await events.group.deleted(group)
ctx.body = { message: "Group deleted successfully" } ctx.body = { message: "Group deleted successfully" }
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status, err) ctx.throw(err.status, err)

View File

@ -3,7 +3,7 @@ import { checkInviteCode } from "../../../utilities/redis"
import { sendEmail } from "../../../utilities/email" import { sendEmail } from "../../../utilities/email"
import { users } from "../../../sdk" import { users } from "../../../sdk"
import env from "../../../environment" import env from "../../../environment"
import { User, CloudAccount } from "@budibase/types" import { User, CloudAccount, UserGroup } from "@budibase/types"
import { import {
events, events,
errors, errors,
@ -24,53 +24,54 @@ export const save = async (ctx: any) => {
export const bulkSave = async (ctx: any) => { export const bulkSave = async (ctx: any) => {
let { users: newUsersRequested, groups } = ctx.request.body let { users: newUsersRequested, groups } = ctx.request.body
let usersToSave: any[] = []
let groupsToSave: any[] = [] let groupsToSave: any[] = []
const newUsers: any[] = [] const newUsers: any[] = []
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const currentUserEmails = const currentUserEmails =
(await users.allUsers())?.map((x: any) => x.email) || [] (await users.allUsers())?.map((x: any) => x.email) || []
for (const newUser of newUsersRequested) { for (const newUser of newUsersRequested) {
if ( if (
newUsers.find((x: any) => x.email === newUser.email) || newUsers.find((x: any) => x.email === newUser.email) ||
currentUserEmails.includes(newUser.email) currentUserEmails.includes(newUser.email)
) )
continue continue
newUser.userGroups = groups
newUsers.push(newUser) newUsers.push(newUser)
} }
newUsers.forEach((user: any) => { if (groups.length) {
usersToSave.push( groups.forEach(async (groupId: string) => {
users.save(user, { let oldGroup = await db.get(groupId)
groupsToSave.push(oldGroup)
})
}
try {
let response = []
for (const user of newUsers) {
response = await users.save(user, {
hashPassword: true, hashPassword: true,
requirePassword: user.requirePassword, requirePassword: user.requirePassword,
bulkCreate: true, bulkCreate: false,
})
)
if (groups.length) {
groups.forEach(async (groupId: string) => {
let oldGroup = await db.get(groupId)
groupsToSave.push(oldGroup)
}) })
} }
})
try {
const allUsers = await Promise.all(usersToSave)
let response = await db.bulkDocs(allUsers)
// delete passwords and add to group // delete passwords and add to group
allUsers.forEach(user => { newUsers.forEach(user => {
delete user.password delete user.password
}) })
if (groupsToSave.length) if (groupsToSave.length) {
groupsToSave.forEach(async group => { groupsToSave.forEach(async (userGroup: UserGroup) => {
group.users = [...group.users, ...allUsers] userGroup.users = [...userGroup.users, ...newUsers]
await db.put(group) await db.put(userGroup)
events.group.usersAdded(
newUsers.map(u => u.email),
userGroup
)
events.group.createdOnboarding(userGroup._id as string)
}) })
}
ctx.body = response ctx.body = response
} catch (err: any) { } catch (err: any) {

View File

@ -31,9 +31,8 @@ export const allUsers = async () => {
export const paginatedUsers = async ({ export const paginatedUsers = async ({
page, page,
email, search,
appId, }: { page?: string; search?: string } = {}) => {
}: { page?: string; email?: string; appId?: string } = {}) => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
// get one extra document, to have the next page // get one extra document, to have the next page
const opts: any = { const opts: any = {
@ -45,24 +44,19 @@ export const paginatedUsers = async ({
opts.startkey = page opts.startkey = page
} }
// property specifies what to use for the page/anchor // property specifies what to use for the page/anchor
let userList, let userList, property
property = "_id", // no search, query allDocs
getKey if (!search) {
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
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts)) const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
userList = response.rows.map((row: any) => row.doc) 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, { return dbUtils.pagination(userList, PAGE_LIMIT, {
paginate: true, paginate: true,
property, property,
getKey,
}) })
} }