From b4c88bd545b1edea79f95deded40b35ae96c86e1 Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 23 Feb 2023 10:38:03 +0000 Subject: [PATCH] Merge commit to dev --- packages/types/src/api/web/user.ts | 1 + .../src/api/controllers/global/users.ts | 119 +++++++++++++++++- packages/worker/src/sdk/users/users.ts | 6 +- packages/worker/src/utilities/redis.ts | 40 ++++++ 4 files changed, 159 insertions(+), 7 deletions(-) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 6acaf6912d..18f98048c4 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -16,6 +16,7 @@ export interface BulkUserRequest { userIds: string[] } create?: { + roles?: any[] users: User[] groups: any[] } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 817480151d..f8cb4b2103 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,4 +1,8 @@ -import { checkInviteCode } from "../../../utilities/redis" +import { + checkInviteCode, + getInviteCodes, + updateInviteCode, +} from "../../../utilities/redis" import sdk from "../../../sdk" import env from "../../../environment" import { @@ -6,7 +10,6 @@ import { BulkUserResponse, CloudAccount, CreateAdminUserRequest, - InviteUserRequest, InviteUsersRequest, SearchUsersRequest, User, @@ -19,6 +22,7 @@ import { tenancy, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" +import { isEmailConfigured } from "src/utilities/email" const MAX_USERS_UPLOAD_LIMIT = 1000 @@ -186,9 +190,54 @@ export const tenantUserLookup = async (ctx: any) => { } } +/* + Encapsulate the app user onboarding flows here. +*/ +export const onboardUsers = async (ctx: any) => { + const request = ctx.request.body as InviteUsersRequest | BulkUserRequest + const isBulkCreate = "create" in request + + const emailConfigured = await isEmailConfigured() + + let onboardingResponse + + if (isBulkCreate) { + // @ts-ignore + const { users, groups, roles } = request.create + const assignUsers = users.map((user: User) => (user.roles = roles)) + onboardingResponse = await sdk.users.bulkCreate(assignUsers, groups) + ctx.body = onboardingResponse + } else if (emailConfigured) { + onboardingResponse = await invite(ctx) + } else if (!emailConfigured) { + const inviteRequest = ctx.request.body as InviteUsersRequest + const users: User[] = inviteRequest.map(invite => { + let password = Math.random().toString(36).substring(2, 22) + + return { + email: invite.email, + password, + forceResetPassword: true, + roles: invite.userInfo.apps, + admin: { global: false }, + builder: { global: false }, + tenantId: tenancy.getTenantId(), + } + }) + let bulkCreateReponse = await sdk.users.bulkCreate(users, []) + onboardingResponse = { + ...bulkCreateReponse, + created: true, + } + ctx.body = onboardingResponse + } else { + ctx.throw(400, "User onboarding failed") + } +} + export const invite = async (ctx: any) => { - const request = ctx.request.body as InviteUserRequest - const response = await sdk.users.invite([request]) + const request = ctx.request.body as InviteUsersRequest + const response = await sdk.users.invite(request) // explicitly throw for single user invite if (response.unsuccessful.length) { @@ -202,6 +251,8 @@ export const invite = async (ctx: any) => { ctx.body = { message: "Invitation has been sent.", + successful: response.successful, + unsuccessful: response.unsuccessful, } } @@ -223,19 +274,75 @@ export const checkInvite = async (ctx: any) => { } } +export const getUserInvites = async (ctx: any) => { + let invites + try { + // Restricted to the currently authenticated tenant + invites = await getInviteCodes([ctx.user.tenantId]) + } catch (e) { + ctx.throw(400, "There was a problem fetching invites") + } + ctx.body = invites +} + +export const updateInvite = async (ctx: any) => { + const { code } = ctx.params + let updateBody = { ...ctx.request.body } + + delete updateBody.email + + let invite + try { + invite = await checkInviteCode(code, false) + if (!invite) { + throw new Error("The invite could not be retrieved") + } + } catch (e) { + ctx.throw(400, "There was a problem with the invite") + } + + let updated = { + ...invite, + } + + if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) { + updated.info.apps = [] + } else { + updated.info = { + ...invite.info, + apps: { + ...invite.info.apps, + ...updateBody.apps, + }, + } + } + + await updateInviteCode(code, updated) + ctx.body = { ...invite } +} + export const inviteAccept = async (ctx: any) => { const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) ctx.body = await tenancy.doInTenant(info.tenantId, async () => { - const saved = await sdk.users.save({ + let request = { firstName, lastName, password, email, + roles: info.apps, + } + + delete info.apps + + request = { + ...request, ...info, - }) + } + + const saved = await sdk.users.save(request) const db = tenancy.getGlobalDB() const user = await db.get(saved._id) await events.user.inviteAccepted(user) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 5124a5c5b1..db49e801c3 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -203,7 +203,7 @@ export const save = async ( const tenantId = tenancy.getTenantId() const db = tenancy.getGlobalDB() - let { email, _id, userGroups = [] } = user + let { email, _id, userGroups = [], roles } = user if (!email && !_id) { throw new Error("_id or email is required") @@ -245,6 +245,10 @@ export const save = async ( builtUser.roles = dbUser.roles } + if (!dbUser && roles?.length) { + builtUser.roles = { ...roles } + } + // make sure we set the _id field for a new user // Also if this is a new user, associate groups with them let groupPromises = [] diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 893ec9f0a8..a05b043054 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -29,6 +29,20 @@ async function writeACode(db: string, value: any) { return code } +async function updateACode(db: string, code: string, value: any) { + const client = await getClient(db) + await client.store(code, value, getExpirySecondsForDB(db)) +} + +/** + * Given an invite code and invite body, allow the update an existing/valid invite in redis + * @param {string} inviteCode The invite code for an invite in redis + * @param {object} value The body of the updated user invitation + */ +export async function updateInviteCode(inviteCode: string, value: string) { + await updateACode(redis.utils.Databases.INVITATIONS, inviteCode, value) +} + async function getACode(db: string, code: string, deleteCode = true) { const client = await getClient(db) const value = await client.get(code) @@ -111,3 +125,29 @@ export async function checkInviteCode( throw "Invitation is not valid or has expired, please request a new one." } } + +/** + Get all currently available user invitations. + @return {Object[]} A +**/ +export async function getInviteCodes( + tenantIds?: string[] //should default to the current tenant of the user session. +) { + const client = await getClient(redis.utils.Databases.INVITATIONS) + const invites: any[] = await client.scan() + + const results = invites.map(invite => { + return { + ...invite.value, + code: invite.key, + } + }) + return results.reduce((acc, invite) => { + if (tenantIds?.length && tenantIds.includes(invite.info.tenantId)) { + acc.push(invite) + } else { + acc.push(invite) + } + return acc + }, []) +}