diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 37abd7f1eb..2260892913 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -442,13 +442,11 @@ const onUpdateUserInvite = async (invite, role) => { let updateBody = { - code: invite.code, apps: { ...invite.apps, [prodAppId]: role, }, } - if (role === Constants.Roles.CREATOR) { updateBody.builder = updateBody.builder || {} updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId] @@ -456,7 +454,7 @@ } else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) { invite.builder.apps = [] } - await users.updateInvite(updateBody) + await users.updateInvite(invite.code, updateBody) await filterInvites(query) } @@ -470,8 +468,7 @@ let updated = { ...invite } delete updated.info.apps[prodAppId] - return await users.updateInvite({ - code: updated.code, + return await users.updateInvite(updated.code, { apps: updated.apps, }) } diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index f94bad2147..bcd59cd948 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -191,8 +191,14 @@ ? "View errors" : "View error"} on:dismiss={async () => { - await automationStore.actions.clearLogErrors({ appId }) - await appsStore.load() + const automationId = Object.keys(automationErrors[appId] || {})[0] + if (automationId) { + await automationStore.actions.clearLogErrors({ + appId, + automationId, + }) + await appsStore.load() + } }} message={automationErrorMessage(appId)} /> diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte index 8d99d406fd..71fd4c0be3 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -52,7 +52,7 @@ ] const removeUser = async id => { - await groups.actions.removeUser(groupId, id) + await groups.removeUser(groupId, id) fetchGroupUsers.refresh() } diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index 97120c55d4..c77e40c964 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -251,6 +251,7 @@ passwordModal.show() await fetch.refresh() } catch (error) { + console.error(error) notifications.error("Error creating user") } } diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.ts similarity index 52% rename from packages/builder/src/stores/portal/users.js rename to packages/builder/src/stores/portal/users.ts index 99ead22317..605f8612aa 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.ts @@ -1,41 +1,71 @@ -import { writable } from "svelte/store" import { API } from "@/api" -import { update } from "lodash" import { licensing } from "." import { sdk } from "@budibase/shared-core" import { Constants } from "@budibase/frontend-core" +import { + DeleteInviteUsersRequest, + InviteUsersRequest, + SearchUsersRequest, + SearchUsersResponse, + UpdateInviteRequest, + User, + UserIdentifier, + UnsavedUser, +} from "@budibase/types" +import { BudiStore } from "../BudiStore" -export function createUsersStore() { - const { subscribe, set } = writable({}) +interface UserInfo { + email: string + password: string + forceResetPassword?: boolean + role: keyof typeof Constants.BudibaseRoles +} - // opts can contain page and search params - async function search(opts = {}) { +type UserState = SearchUsersResponse & SearchUsersRequest + +class UserStore extends BudiStore { + constructor() { + super({ + data: [], + }) + } + + async search(opts: SearchUsersRequest = {}) { const paged = await API.searchUsers(opts) - set({ + this.set({ ...paged, ...opts, }) return paged } - async function get(userId) { + async get(userId: string) { try { return await API.getUser(userId) } catch (err) { return null } } - const fetch = async () => { + + async fetch() { return await API.getUsers() } - // One or more users. - async function onboard(payload) { + async onboard(payload: InviteUsersRequest) { return await API.onboardUsers(payload) } - async function invite(payload) { - const users = payload.map(user => { + async invite( + payload: { + admin?: boolean + builder?: boolean + creator?: boolean + email: string + apps?: any[] + groups?: any[] + }[] + ) { + const users: InviteUsersRequest = payload.map(user => { let builder = undefined if (user.admin || user.builder) { builder = { global: true } @@ -55,11 +85,16 @@ export function createUsersStore() { return API.inviteUsers(users) } - async function removeInvites(payload) { + async removeInvites(payload: DeleteInviteUsersRequest) { return API.removeUserInvites(payload) } - async function acceptInvite(inviteCode, password, firstName, lastName) { + async acceptInvite( + inviteCode: string, + password: string, + firstName: string, + lastName?: string + ) { return API.acceptInvite({ inviteCode, password, @@ -68,21 +103,25 @@ export function createUsersStore() { }) } - async function fetchInvite(inviteCode) { + async fetchInvite(inviteCode: string) { return API.getUserInvite(inviteCode) } - async function getInvites() { + async getInvites() { return API.getUserInvites() } - async function updateInvite(invite) { - return API.updateUserInvite(invite.code, invite) + async updateInvite(code: string, invite: UpdateInviteRequest) { + return API.updateUserInvite(code, invite) } - async function create(data) { - let mappedUsers = data.users.map(user => { - const body = { + async getUserCountByApp(appId: string) { + return await API.getUserCountByApp(appId) + } + + async create(data: { users: UserInfo[]; groups: any[] }) { + let mappedUsers: UnsavedUser[] = data.users.map((user: any) => { + const body: UnsavedUser = { email: user.email, password: user.password, roles: {}, @@ -92,17 +131,17 @@ export function createUsersStore() { } switch (user.role) { - case "appUser": + case Constants.BudibaseRoles.AppUser: body.builder = { global: false } body.admin = { global: false } break - case "developer": + case Constants.BudibaseRoles.Developer: body.builder = { global: true } break - case "creator": + case Constants.BudibaseRoles.Creator: body.builder = { creator: true, global: false } break - case "admin": + case Constants.BudibaseRoles.Admin: body.admin = { global: true } body.builder = { global: true } break @@ -111,43 +150,47 @@ export function createUsersStore() { return body }) const response = await API.createUsers(mappedUsers, data.groups) + licensing.setQuotaUsage() // re-search from first page - await search() + await this.search() return response } - async function del(id) { + async delete(id: string) { await API.deleteUser(id) - update(users => users.filter(user => user._id !== id)) + licensing.setQuotaUsage() } - async function getUserCountByApp(appId) { - return await API.getUserCountByApp(appId) + async bulkDelete(users: UserIdentifier[]) { + const res = API.deleteUsers(users) + licensing.setQuotaUsage() + return res } - async function bulkDelete(users) { - return API.deleteUsers(users) + async save(user: User) { + const res = await API.saveUser(user) + licensing.setQuotaUsage() + return res } - async function save(user) { - return await API.saveUser(user) - } - - async function addAppBuilder(userId, appId) { + async addAppBuilder(userId: string, appId: string) { return await API.addAppBuilder(userId, appId) } - async function removeAppBuilder(userId, appId) { + async removeAppBuilder(userId: string, appId: string) { return await API.removeAppBuilder(userId, appId) } - async function getAccountHolder() { + async getAccountHolder() { return await API.getAccountHolder() } - const getUserRole = user => { - if (user && user.email === user.tenantOwnerEmail) { + getUserRole(user?: User & { tenantOwnerEmail?: string }) { + if (!user) { + return Constants.BudibaseRoles.AppUser + } + if (user.email === user.tenantOwnerEmail) { return Constants.BudibaseRoles.Owner } else if (sdk.users.isAdmin(user)) { return Constants.BudibaseRoles.Admin @@ -159,38 +202,6 @@ export function createUsersStore() { return Constants.BudibaseRoles.AppUser } } - - const refreshUsage = - fn => - async (...args) => { - const response = await fn(...args) - await licensing.setQuotaUsage() - return response - } - - return { - subscribe, - search, - get, - getUserRole, - fetch, - invite, - onboard, - fetchInvite, - getInvites, - removeInvites, - updateInvite, - getUserCountByApp, - addAppBuilder, - removeAppBuilder, - // any operation that adds or deletes users - acceptInvite, - create: refreshUsage(create), - save: refreshUsage(save), - bulkDelete: refreshUsage(bulkDelete), - delete: refreshUsage(del), - getAccountHolder, - } } -export const users = createUsersStore() +export const users = new UserStore() diff --git a/packages/frontend-core/src/api/user.ts b/packages/frontend-core/src/api/user.ts index 84ec68644d..cf66751078 100644 --- a/packages/frontend-core/src/api/user.ts +++ b/packages/frontend-core/src/api/user.ts @@ -21,11 +21,12 @@ import { SaveUserResponse, SearchUsersRequest, SearchUsersResponse, + UnsavedUser, UpdateInviteRequest, UpdateInviteResponse, UpdateSelfMetadataRequest, UpdateSelfMetadataResponse, - User, + UserIdentifier, } from "@budibase/types" import { BaseAPIClient } from "./types" @@ -38,14 +39,9 @@ export interface UserEndpoints { createAdminUser: ( user: CreateAdminUserRequest ) => Promise - saveUser: (user: User) => Promise + saveUser: (user: UnsavedUser) => Promise deleteUser: (userId: string) => Promise - deleteUsers: ( - users: Array<{ - userId: string - email: string - }> - ) => Promise + deleteUsers: (users: UserIdentifier[]) => Promise onboardUsers: (data: InviteUsersRequest) => Promise getUserInvite: (code: string) => Promise getUserInvites: () => Promise @@ -60,7 +56,7 @@ export interface UserEndpoints { getAccountHolder: () => Promise searchUsers: (data: SearchUsersRequest) => Promise createUsers: ( - users: User[], + users: UnsavedUser[], groups: any[] ) => Promise updateUserInvite: ( diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index a42449d550..c1f37fd3f0 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -22,6 +22,8 @@ export interface UserDetails { password?: string } +export type UnsavedUser = Omit + export interface BulkUserRequest { delete?: { users: Array<{ @@ -31,7 +33,7 @@ export interface BulkUserRequest { } create?: { roles?: any[] - users: User[] + users: UnsavedUser[] groups: any[] } } @@ -124,7 +126,7 @@ export interface AcceptUserInviteRequest { inviteCode: string password: string firstName: string - lastName: string + lastName?: string } export interface AcceptUserInviteResponse { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 83f2f41b0e..0bcdadfefc 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -33,6 +33,7 @@ import { SaveUserResponse, SearchUsersRequest, SearchUsersResponse, + UnsavedUser, UpdateInviteRequest, UpdateInviteResponse, User, @@ -49,6 +50,7 @@ import { tenancy, db, locks, + context, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { isEmailConfigured } from "../../../utilities/email" @@ -66,10 +68,11 @@ const generatePassword = (length: number) => { .slice(0, length) } -export const save = async (ctx: UserCtx) => { +export const save = async (ctx: UserCtx) => { try { const currentUserId = ctx.user?._id - const requestUser = ctx.request.body + const tenantId = context.getTenantId() + const requestUser: User = { ...ctx.request.body, tenantId } // Do not allow the account holder role to be changed if ( @@ -151,7 +154,12 @@ export const bulkUpdate = async ( let created, deleted try { if (input.create) { - created = await bulkCreate(input.create.users, input.create.groups) + const tenantId = context.getTenantId() + const users: User[] = input.create.users.map(user => ({ + ...user, + tenantId, + })) + created = await bulkCreate(users, input.create.groups) } if (input.delete) { deleted = await bulkDelete(input.delete.users, currentUserId)