@@ -250,10 +250,6 @@
flex-direction: row;
justify-content: flex-end;
align-items: center;
- gap: var(--spacing-xl);
- }
-
- .version {
- margin-right: var(--spacing-s);
+ gap: var(--spacing-l);
}
diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js
index 1510207604..206e14568b 100644
--- a/packages/builder/src/stores/portal/users.js
+++ b/packages/builder/src/stores/portal/users.js
@@ -26,9 +26,15 @@ export function createUsersStore() {
return await API.getUsers()
}
+ // One or more users.
+ async function onboard(payload) {
+ return await API.onboardUsers(payload)
+ }
+
async function invite(payload) {
return API.inviteUsers(payload)
}
+
async function acceptInvite(inviteCode, password, firstName, lastName) {
return API.acceptInvite({
inviteCode,
@@ -42,6 +48,14 @@ export function createUsersStore() {
return API.getUserInvite(inviteCode)
}
+ async function getInvites() {
+ return API.getUserInvites()
+ }
+
+ async function updateInvite(invite) {
+ return API.updateUserInvite(invite)
+ }
+
async function create(data) {
let mappedUsers = data.users.map(user => {
const body = {
@@ -106,8 +120,11 @@ export function createUsersStore() {
getUserRole,
fetch,
invite,
+ onboard,
acceptInvite,
fetchInvite,
+ getInvites,
+ updateInvite,
create,
save,
bulkDelete,
diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js
index 9875605ce0..cb8a8f6555 100644
--- a/packages/frontend-core/src/api/user.js
+++ b/packages/frontend-core/src/api/user.js
@@ -12,8 +12,10 @@ export const buildUserEndpoints = API => ({
* Gets a list of users in the current tenant.
* @param {string} page The page to retrieve
* @param {string} search The starts with string to search username/email by.
+ * @param {string} appId Facilitate app/role based user searching
+ * @param {boolean} paginated Allow the disabling of pagination
*/
- searchUsers: async ({ page, email, appId } = {}) => {
+ searchUsers: async ({ paginated, page, email, appId } = {}) => {
const opts = {}
if (page) {
opts.page = page
@@ -24,6 +26,9 @@ export const buildUserEndpoints = API => ({
if (appId) {
opts.appId = appId
}
+ if (typeof paginated === "boolean") {
+ opts.paginated = paginated
+ }
return await API.post({
url: `/api/global/users/search`,
body: opts,
@@ -133,7 +138,7 @@ export const buildUserEndpoints = API => ({
* @param builder whether the user should be a global builder
* @param admin whether the user should be a global admin
*/
- inviteUser: async ({ email, builder, admin }) => {
+ inviteUser: async ({ email, builder, admin, apps }) => {
return await API.post({
url: "/api/global/users/invite",
body: {
@@ -141,11 +146,43 @@ export const buildUserEndpoints = API => ({
userInfo: {
admin: admin ? { global: true } : undefined,
builder: builder ? { global: true } : undefined,
+ apps: apps ? apps : undefined,
},
},
})
},
+ onboardUsers: async payload => {
+ return await API.post({
+ url: "/api/global/users/onboard",
+ body: payload.map(invite => {
+ const { email, admin, builder, apps } = invite
+ return {
+ email,
+ userInfo: {
+ admin: admin ? { global: true } : undefined,
+ builder: builder ? { global: true } : undefined,
+ apps: apps ? apps : undefined,
+ },
+ }
+ }),
+ })
+ },
+
+ /**
+ * Accepts a user invite as a body and will update the associated app roles.
+ * for an existing invite
+ * @param invite the invite code sent in the email
+ */
+ updateUserInvite: async invite => {
+ await API.post({
+ url: `/api/global/users/invite/update/${invite.code}`,
+ body: {
+ apps: invite.apps,
+ },
+ })
+ },
+
/**
* Retrieves the invitation associated with a provided code.
* @param code The unique code for the target invite
@@ -156,6 +193,16 @@ export const buildUserEndpoints = API => ({
})
},
+ /**
+ * Retrieves the invitation associated with a provided code.
+ * @param code The unique code for the target invite
+ */
+ getUserInvites: async () => {
+ return await API.get({
+ url: `/api/global/users/invites`,
+ })
+ },
+
/**
* Invites multiple users to the current tenant.
* @param users An array of users to invite
@@ -169,6 +216,7 @@ export const buildUserEndpoints = API => ({
admin: user.admin ? { global: true } : undefined,
builder: user.admin || user.builder ? { global: true } : undefined,
userGroups: user.groups,
+ roles: user.apps ? user.apps : undefined,
},
})),
})
diff --git a/packages/frontend-core/src/fetch/UserFetch.js b/packages/frontend-core/src/fetch/UserFetch.js
index 9aeadbc0f5..5372d0ec33 100644
--- a/packages/frontend-core/src/fetch/UserFetch.js
+++ b/packages/frontend-core/src/fetch/UserFetch.js
@@ -35,6 +35,7 @@ export default class UserFetch extends DataFetch {
page: cursor,
email: query.email,
appId: query.appId,
+ paginated: query.paginated,
})
return {
rows: res?.data || [],
diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts
index b2ac6b7804..63d6be6fa0 100644
--- a/packages/types/src/api/web/user.ts
+++ b/packages/types/src/api/web/user.ts
@@ -50,7 +50,7 @@ export interface SearchUsersRequest {
page?: string
email?: string
appId?: string
- userIds?: string[]
+ paginated?: boolean
}
export interface CreateAdminUserRequest {
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index e41a280b22..2971011682 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -185,16 +185,28 @@ export const destroy = async (ctx: any) => {
}
}
+export const getAppUsers = async (ctx: any) => {
+ const body = ctx.request.body as SearchUsersRequest
+ const users = await userSdk.getUsersByAppAccess(body?.appId)
+
+ ctx.body = { data: users }
+}
+
export const search = async (ctx: any) => {
const body = ctx.request.body as SearchUsersRequest
- const paginated = await userSdk.paginatedUsers(body)
- // user hashed password shouldn't ever be returned
- for (let user of paginated.data) {
- if (user) {
- delete user.password
+
+ if (body.paginated === false) {
+ await getAppUsers(ctx)
+ } else {
+ const paginated = await userSdk.paginatedUsers(body)
+ // user hashed password shouldn't ever be returned
+ for (let user of paginated.data) {
+ if (user) {
+ delete user.password
+ }
}
+ ctx.body = paginated
}
- ctx.body = paginated
}
// called internally by app server user fetch
@@ -242,12 +254,18 @@ export const onboardUsers = async (ctx: any) => {
onboardingResponse = await userSdk.bulkCreate(assignUsers, groups)
ctx.body = onboardingResponse
} else if (emailConfigured) {
- onboardingResponse = await inviteMultiple(ctx)
+ onboardingResponse = await invite(ctx)
} else if (!emailConfigured) {
const inviteRequest = ctx.request.body as InviteUsersRequest
+
+ let createdPasswords: any = {}
+
const users: User[] = inviteRequest.map(invite => {
let password = Math.random().toString(36).substring(2, 22)
+ // Temp password to be passed to the user.
+ createdPasswords[invite.email] = password
+
return {
email: invite.email,
password,
@@ -259,19 +277,28 @@ export const onboardUsers = async (ctx: any) => {
}
})
let bulkCreateReponse = await userSdk.bulkCreate(users, [])
- onboardingResponse = {
+
+ // Apply temporary credentials
+ let createWithCredentials = {
...bulkCreateReponse,
+ successful: bulkCreateReponse?.successful.map(user => {
+ return {
+ ...user,
+ password: createdPasswords[user.email],
+ }
+ }),
created: true,
}
- ctx.body = onboardingResponse
+
+ ctx.body = createWithCredentials
} else {
ctx.throw(400, "User onboarding failed")
}
}
export const invite = async (ctx: any) => {
- const request = ctx.request.body as InviteUserRequest
- const response = await userSdk.invite([request])
+ const request = ctx.request.body as InviteUsersRequest
+ const response = await userSdk.invite(request)
// explicitly throw for single user invite
if (response.unsuccessful.length) {
diff --git a/packages/worker/src/api/routes/global/users.ts b/packages/worker/src/api/routes/global/users.ts
index a73462b235..db182a99c6 100644
--- a/packages/worker/src/api/routes/global/users.ts
+++ b/packages/worker/src/api/routes/global/users.ts
@@ -38,13 +38,6 @@ function buildInviteMultipleValidation() {
))
}
-function buildInviteLookupValidation() {
- // prettier-ignore
- return auth.joiValidator.params(Joi.object({
- code: Joi.string().required()
- }).unknown(true))
-}
-
const createUserAdminOnly = (ctx: any, next: any) => {
if (!ctx.request.body._id) {
return auth.adminOnly(ctx, next)
@@ -88,22 +81,34 @@ router
.get("/api/global/roles/:appId")
.post(
"/api/global/users/invite",
- auth.adminOnly,
+ auth.builderOrAdmin,
buildInviteValidation(),
controller.invite
)
+ .post(
+ "/api/global/users/onboard",
+ auth.builderOrAdmin,
+ buildInviteMultipleValidation(),
+ controller.onboardUsers
+ )
.post(
"/api/global/users/multi/invite",
- auth.adminOnly,
+ auth.builderOrAdmin,
buildInviteMultipleValidation(),
controller.inviteMultiple
)
// non-global endpoints
+ .get("/api/global/users/invite/:code", controller.checkInvite)
+ .post(
+ "/api/global/users/invite/update/:code",
+ auth.builderOrAdmin,
+ controller.updateInvite
+ )
.get(
- "/api/global/users/invite/:code",
- buildInviteLookupValidation(),
- controller.checkInvite
+ "/api/global/users/invites",
+ auth.builderOrAdmin,
+ controller.getUserInvites
)
.post(
"/api/global/users/invite/accept",
diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts
index 4a72badc10..de1ea27546 100644
--- a/packages/worker/src/sdk/users/users.ts
+++ b/packages/worker/src/sdk/users/users.ts
@@ -56,11 +56,22 @@ export const countUsersByApp = async (appId: string) => {
}
}
+export const getUsersByAppAccess = async (appId?: string) => {
+ const opts: any = {
+ include_docs: true,
+ limit: 50,
+ }
+ let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
+ appId,
+ opts
+ )
+ return response
+}
+
export const paginatedUsers = async ({
page,
email,
appId,
- userIds,
}: SearchUsersRequest = {}) => {
const db = tenancy.getGlobalDB()
// get one extra document, to have the next page
diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts
index e9ba06227a..1e0d0beb97 100644
--- a/packages/worker/src/utilities/redis.ts
+++ b/packages/worker/src/utilities/redis.ts
@@ -130,11 +130,9 @@ export async function checkInviteCode(
/**
Get all currently available user invitations.
- @return {Object[]} A
+ @return {Object[]} A list of all objects containing invite metadata
**/
-export async function getInviteCodes(
- tenantIds?: string[] //should default to the current tenant of the user session.
-) {
+export async function getInviteCodes(tenantIds?: string[]) {
const client = await getClient(redis.utils.Databases.INVITATIONS)
const invites: any[] = await client.scan()