From 52916f11a8129cd8dec61dcd73fc30c2f894f741 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Jan 2025 14:06:03 +0000 Subject: [PATCH 1/8] Convert portal user store to TS --- .../_components/BuilderSidePanel.svelte | 7 +- .../src/stores/portal/{users.js => users.ts} | 144 ++++++++++-------- packages/frontend-core/src/api/user.ts | 2 +- packages/types/src/api/web/user.ts | 2 +- 4 files changed, 85 insertions(+), 70 deletions(-) rename packages/builder/src/stores/portal/{users.js => users.ts} (55%) 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/stores/portal/users.js b/packages/builder/src/stores/portal/users.ts similarity index 55% rename from packages/builder/src/stores/portal/users.js rename to packages/builder/src/stores/portal/users.ts index 99ead22317..7c0bec296e 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.ts @@ -1,41 +1,68 @@ -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, +} from "@budibase/types" +import { BudiStore } from "../BudiStore" -export function createUsersStore() { - const { subscribe, set } = writable({}) +type UserState = SearchUsersResponse & SearchUsersRequest - // opts can contain page and search params - async function search(opts = {}) { +class UserStore extends BudiStore { + constructor() { + super({ + data: [], + }) + + // Update quotas after any add or remove operation + this.create = this.refreshUsage(this.create) + this.save = this.refreshUsage(this.save) + this.delete = this.refreshUsage(this.delete) + this.bulkDelete = this.refreshUsage(this.bulkDelete) + } + + 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 +82,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 +100,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: any) { + let mappedUsers: Omit[] = data.users.map((user: any) => { + const body: Omit = { email: user.email, password: user.password, roles: {}, @@ -113,41 +149,44 @@ export function createUsersStore() { const response = await API.createUsers(mappedUsers, data.groups) // 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)) } - async function getUserCountByApp(appId) { - return await API.getUserCountByApp(appId) - } - - async function bulkDelete(users) { + async bulkDelete( + users: Array<{ + userId: string + email: string + }> + ) { return API.deleteUsers(users) } - async function save(user) { + async save(user: 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 @@ -160,37 +199,16 @@ export function createUsersStore() { } } - const refreshUsage = - fn => - async (...args) => { + foo = this.refreshUsage(this.create) + bar = this.refreshUsage(this.save) + + refreshUsage(fn: (...args: T) => Promise) { + return async function (...args: T) { 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..7464b1ec4a 100644 --- a/packages/frontend-core/src/api/user.ts +++ b/packages/frontend-core/src/api/user.ts @@ -60,7 +60,7 @@ export interface UserEndpoints { getAccountHolder: () => Promise searchUsers: (data: SearchUsersRequest) => Promise createUsers: ( - users: User[], + users: Omit[], groups: any[] ) => Promise updateUserInvite: ( diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index a42449d550..8b0dfef34b 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -124,7 +124,7 @@ export interface AcceptUserInviteRequest { inviteCode: string password: string firstName: string - lastName: string + lastName?: string } export interface AcceptUserInviteResponse { From 6bd4cb47c204d1c2626c0d8d93c71b2b695679a4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 8 Jan 2025 15:54:09 +0000 Subject: [PATCH 2/8] Add new UnsavedUser type and update controllers --- packages/builder/src/stores/portal/users.ts | 35 ++++++++++--------- packages/frontend-core/src/api/user.ts | 14 +++----- packages/types/src/api/web/user.ts | 4 ++- .../src/api/controllers/global/users.ts | 16 ++++++--- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 7c0bec296e..9284ad2992 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -9,9 +9,18 @@ import { SearchUsersResponse, UpdateInviteRequest, User, + UserIdentifier, + UnsavedUser, } from "@budibase/types" import { BudiStore } from "../BudiStore" +interface UserInfo { + email: string + password: string + forceResetPassword?: boolean + role: keyof typeof Constants.BudibaseRoles +} + type UserState = SearchUsersResponse & SearchUsersRequest class UserStore extends BudiStore { @@ -116,9 +125,9 @@ class UserStore extends BudiStore { return await API.getUserCountByApp(appId) } - async create(data: any) { - let mappedUsers: Omit[] = data.users.map((user: any) => { - const body: Omit = { + 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: {}, @@ -128,17 +137,17 @@ class UserStore extends BudiStore { } 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 @@ -157,12 +166,7 @@ class UserStore extends BudiStore { await API.deleteUser(id) } - async bulkDelete( - users: Array<{ - userId: string - email: string - }> - ) { + async bulkDelete(users: UserIdentifier[]) { return API.deleteUsers(users) } @@ -199,9 +203,8 @@ class UserStore extends BudiStore { } } - foo = this.refreshUsage(this.create) - bar = this.refreshUsage(this.save) - + // Wrapper function to refresh quota usage after an operation, + // persisting argument and return types refreshUsage(fn: (...args: T) => Promise) { return async function (...args: T) { const response = await fn(...args) diff --git a/packages/frontend-core/src/api/user.ts b/packages/frontend-core/src/api/user.ts index 7464b1ec4a..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: Omit[], + 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 8b0dfef34b..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[] } } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index a028f4fd33..4f9135e79e 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 const accountMetadata = await users.getExistingAccounts([requestUser.email]) @@ -149,7 +152,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) @@ -441,7 +449,6 @@ export const checkInvite = async (ctx: UserCtx) => { } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") - return } ctx.body = { email: invite.email, @@ -472,7 +479,6 @@ export const updateInvite = async ( invite = await cache.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") - return } let updated = { From fcb3a3a1986eae17208ecf44cfa0d9b3e513d09c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:24:23 +0000 Subject: [PATCH 3/8] Fix this reference --- packages/builder/src/stores/portal/users.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 9284ad2992..1beb73a6c0 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -29,6 +29,8 @@ class UserStore extends BudiStore { data: [], }) + this.search = this.search.bind(this) + // Update quotas after any add or remove operation this.create = this.refreshUsage(this.create) this.save = this.refreshUsage(this.save) From a6ac76eb05f37c2402cd86c693334b36c6bee757 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:24:37 +0000 Subject: [PATCH 4/8] Fix group actions error --- .../builder/portal/users/groups/_components/GroupUsers.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() } From 1ec4b4c6b7fc3f0a7f91b52797669612c061761e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:38:00 +0000 Subject: [PATCH 5/8] Fix this reference --- .../src/pages/builder/portal/users/users/index.svelte | 1 + packages/builder/src/stores/portal/users.ts | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) 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.ts b/packages/builder/src/stores/portal/users.ts index 1beb73a6c0..6503fc9280 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -29,13 +29,11 @@ class UserStore extends BudiStore { data: [], }) - this.search = this.search.bind(this) - // Update quotas after any add or remove operation - this.create = this.refreshUsage(this.create) - this.save = this.refreshUsage(this.save) - this.delete = this.refreshUsage(this.delete) - this.bulkDelete = this.refreshUsage(this.bulkDelete) + this.create = this.refreshUsage(this.create.bind(this)) + this.save = this.refreshUsage(this.save.bind(this)) + this.delete = this.refreshUsage(this.delete.bind(this)) + this.bulkDelete = this.refreshUsage(this.bulkDelete.bind(this)) } async search(opts: SearchUsersRequest = {}) { From bd378f0bd44f24bdd22c7fa55657ff96f4051d62 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:50:23 +0000 Subject: [PATCH 6/8] Simplify usage quota refreshing when doing user CRUD --- packages/builder/src/stores/portal/users.ts | 26 +++++++-------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/stores/portal/users.ts b/packages/builder/src/stores/portal/users.ts index 6503fc9280..605f8612aa 100644 --- a/packages/builder/src/stores/portal/users.ts +++ b/packages/builder/src/stores/portal/users.ts @@ -28,12 +28,6 @@ class UserStore extends BudiStore { super({ data: [], }) - - // Update quotas after any add or remove operation - this.create = this.refreshUsage(this.create.bind(this)) - this.save = this.refreshUsage(this.save.bind(this)) - this.delete = this.refreshUsage(this.delete.bind(this)) - this.bulkDelete = this.refreshUsage(this.bulkDelete.bind(this)) } async search(opts: SearchUsersRequest = {}) { @@ -156,6 +150,7 @@ class UserStore extends BudiStore { return body }) const response = await API.createUsers(mappedUsers, data.groups) + licensing.setQuotaUsage() // re-search from first page await this.search() @@ -164,14 +159,19 @@ class UserStore extends BudiStore { async delete(id: string) { await API.deleteUser(id) + licensing.setQuotaUsage() } async bulkDelete(users: UserIdentifier[]) { - return API.deleteUsers(users) + const res = API.deleteUsers(users) + licensing.setQuotaUsage() + return res } async save(user: User) { - return await API.saveUser(user) + const res = await API.saveUser(user) + licensing.setQuotaUsage() + return res } async addAppBuilder(userId: string, appId: string) { @@ -202,16 +202,6 @@ class UserStore extends BudiStore { return Constants.BudibaseRoles.AppUser } } - - // Wrapper function to refresh quota usage after an operation, - // persisting argument and return types - refreshUsage(fn: (...args: T) => Promise) { - return async function (...args: T) { - const response = await fn(...args) - await licensing.setQuotaUsage() - return response - } - } } export const users = new UserStore() From 53b12075bb1b35bc7839f46384c6b7e907a92d15 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:57:10 +0000 Subject: [PATCH 7/8] Fix unrelated automation log error message --- .../src/pages/builder/portal/apps/index.svelte | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index f94bad2147..5c3ee674e9 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -176,6 +176,8 @@ notifications.error("Error getting init info") } }) + + $: console.log(automationErrors) @@ -191,8 +193,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)} /> From 79a74ff709706ed37aa3a3a9084f0ba7f4fcb133 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Jan 2025 10:58:31 +0000 Subject: [PATCH 8/8] Remove log --- packages/builder/src/pages/builder/portal/apps/index.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index 5c3ee674e9..bcd59cd948 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -176,8 +176,6 @@ notifications.error("Error getting init info") } }) - - $: console.log(automationErrors)