From 3f69b17c94a6ca45f88764d1ecfb1306b1afc07f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:05:42 +0000 Subject: [PATCH 01/11] Fully type the worker redis utils file. --- .../tests/core/utilities/structures/users.ts | 2 +- .../src/api/controllers/global/users.ts | 15 +- .../src/api/routes/global/tests/users.spec.ts | 21 ++- packages/worker/src/sdk/auth/auth.ts | 2 +- packages/worker/src/sdk/users/users.ts | 2 + packages/worker/src/utilities/email.ts | 6 +- packages/worker/src/utilities/redis.ts | 162 ++++++++++-------- 7 files changed, 124 insertions(+), 86 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 66d23696e0..68ee29686c 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -12,7 +12,7 @@ import { generator } from "./generator" import { tenant } from "." export const newEmail = () => { - return `${uuid()}@test.com` + return `${uuid()}@example.com` } export const user = (userProps?: Partial>): User => { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index de1a605890..1f4168c00b 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,5 +1,6 @@ import { - checkInviteCode, + getInviteCode, + deleteInviteCode, getInviteCodes, updateInviteCode, } from "../../../utilities/redis" @@ -336,10 +337,11 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await checkInviteCode(code, false) + invite = await getInviteCode(code) } 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, @@ -365,12 +367,10 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await checkInviteCode(code, false) - if (!invite) { - throw new Error("The invite could not be retrieved") - } + invite = await getInviteCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") + return } let updated = { @@ -405,7 +405,8 @@ export const inviteAccept = async ( 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) + const { email, info }: any = await getInviteCode(inviteCode) + await deleteInviteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 846b98a7ae..adcbca9d29 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -1,11 +1,12 @@ import { InviteUsersResponse, User } from "@budibase/types" -jest.mock("nodemailer") import { TestConfiguration, mocks, structures } from "../../../../tests" -const sendMailMock = mocks.email.mock() import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" import * as userSdk from "../../../../sdk/users" +jest.mock("nodemailer") +const sendMailMock = mocks.email.mock() + const accounts = jest.mocked(_accounts) describe("/api/global/users", () => { @@ -54,6 +55,22 @@ describe("/api/global/users", () => { expect(events.user.invited).toBeCalledTimes(0) }) + it("should not invite the same user twice", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + const { code, res } = await config.api.users.sendUserInvite( + sendMailMock, + email, + 400 + ) + + expect(res.body.message).toBe(`Unavailable`) + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(code).toBeUndefined() + expect(events.user.invited).toBeCalledTimes(0) + }) + it("should be able to create new user from invite", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index d989113c3d..e25d34fa5e 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.checkResetPasswordCode(resetCode) + const { userId } = await redis.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 230a8fa146..8f04bb1941 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -2,6 +2,7 @@ import { events, tenancy, users as usersCore } from "@budibase/backend-core" import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" +import { getInviteCodes } from "../..//utilities/redis" export async function invite( users: InviteUsersRequest @@ -14,6 +15,7 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) + const existingInvites = await getInviteCodes() const newUsers = [] // separate duplicates from new users diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index c5b1d9d8ab..a0c02d335c 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,7 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { getResetPasswordCode, getInviteCode } from "./redis" +import { createResetPasswordCode, createInviteCode } from "./redis" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" @@ -61,9 +61,9 @@ async function getLinkCode( ) { switch (purpose) { case EmailTemplatePurpose.PASSWORD_RECOVERY: - return getResetPasswordCode(user._id!, info) + return createResetPasswordCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return getInviteCode(email, info) + return createInviteCode(email, info) default: return null } diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 993cdf97ce..9852fb0467 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -1,60 +1,24 @@ import { redis, utils, tenancy } from "@budibase/backend-core" import env from "../environment" -function getExpirySecondsForDB(db: string) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - // a hour - return 3600 - case redis.utils.Databases.INVITATIONS: - // a week - return 604800 - } +interface Invite { + email: string + info: any } -let pwResetClient: any, invitationClient: any - -function getClient(db: string) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - return pwResetClient - case redis.utils.Databases.INVITATIONS: - return invitationClient - } +interface InviteWithCode extends Invite { + code: string } -async function writeACode(db: string, value: any) { - const client = await getClient(db) - const code = utils.newid() - await client.store(code, value, getExpirySecondsForDB(db)) - return code +interface PasswordReset { + userId: string + info: any } -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 inviteCode The invite code for an invite in redis - * @param 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) - if (!value) { - throw new Error("Invalid code.") - } - if (deleteCode) { - await client.delete(code) - } - return value -} +type RedisDBName = + | redis.utils.Databases.PW_RESETS + | redis.utils.Databases.INVITATIONS +let pwResetClient: redis.Client, invitationClient: redis.Client export async function init() { pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS) @@ -63,9 +27,6 @@ export async function init() { await invitationClient.init() } -/** - * make sure redis connection is closed. - */ export async function shutdown() { if (pwResetClient) await pwResetClient.finish() if (invitationClient) await invitationClient.finish() @@ -74,6 +35,65 @@ export async function shutdown() { console.log("Redis shutdown") } +function getExpirySecondsForDB(db: RedisDBName) { + switch (db) { + case redis.utils.Databases.PW_RESETS: + // a hour + return 3600 + case redis.utils.Databases.INVITATIONS: + // a week + return 604800 + default: + throw new Error(`Unknown redis database: ${db}`) + } +} + +function getClient(db: RedisDBName): redis.Client { + switch (db) { + case redis.utils.Databases.PW_RESETS: + return pwResetClient + case redis.utils.Databases.INVITATIONS: + return invitationClient + default: + throw new Error(`Unknown redis database: ${db}`) + } +} + +async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { + const client = getClient(db) + const code = utils.newid() + await client.store(code, value, getExpirySecondsForDB(db)) + return code +} + +async function updateCode(db: RedisDBName, code: string, value: any) { + const client = 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 inviteCode The invite code for an invite in redis + * @param value The body of the updated user invitation + */ +export async function updateInviteCode(code: string, value: Invite) { + await updateCode(redis.utils.Databases.INVITATIONS, code, value) +} + +async function deleteCode(db: RedisDBName, code: string) { + const client = getClient(db) + await client.delete(code) +} + +async function getCode(db: RedisDBName, code: string) { + const client = getClient(db) + const value = await client.get(code) + if (!value) { + throw new Error(`Could not find code: ${code}`) + } + return value +} + /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -81,22 +101,20 @@ export async function shutdown() { * @param info Info about the user/the reset process. * @return returns the code that was stored to redis. */ -export async function getResetPasswordCode(userId: string, info: any) { - return writeACode(redis.utils.Databases.PW_RESETS, { userId, info }) +export async function createResetPasswordCode(userId: string, info: any) { + return writeCode(redis.utils.Databases.PW_RESETS, { userId, info }) } /** - * Given a reset code this will lookup to redis, check if the code is valid and delete if required. + * Given a reset code this will lookup to redis, check if the code is valid. * @param resetCode The code provided via the email link. - * @param deleteCode If the code is used/finished with this will delete it - defaults to true. * @return returns the user ID if it is found */ -export async function checkResetPasswordCode( - resetCode: string, - deleteCode = true -) { +export async function getResetPasswordCode( + code: string +): Promise { try { - return getACode(redis.utils.Databases.PW_RESETS, resetCode, deleteCode) + return getCode(redis.utils.Databases.PW_RESETS, code) } catch (err) { throw "Provided information is not valid, cannot reset password - please try again." } @@ -108,35 +126,35 @@ export async function checkResetPasswordCode( * @param info Information to be carried along with the invitation. * @return returns the code that was stored to redis. */ -export async function getInviteCode(email: string, info: any) { - return writeACode(redis.utils.Databases.INVITATIONS, { email, info }) +export async function createInviteCode(email: string, info: any) { + return writeCode(redis.utils.Databases.INVITATIONS, { email, info }) } /** * Checks that the provided invite code is valid - will return the email address of user that was invited. * @param inviteCode the invite code that was provided as part of the link. - * @param deleteCode whether or not the code should be deleted after retrieval - defaults to true. * @return If the code is valid then an email address will be returned. */ -export async function checkInviteCode( - inviteCode: string, - deleteCode: boolean = true -) { +export async function getInviteCode(code: string): Promise { try { - return getACode(redis.utils.Databases.INVITATIONS, inviteCode, deleteCode) + return getCode(redis.utils.Databases.INVITATIONS, code) } catch (err) { throw "Invitation is not valid or has expired, please request a new one." } } +export async function deleteInviteCode(code: string) { + return deleteCode(redis.utils.Databases.INVITATIONS, code) +} + /** Get all currently available user invitations for the current tenant. **/ -export async function getInviteCodes() { - const client = await getClient(redis.utils.Databases.INVITATIONS) - const invites: any[] = await client.scan() +export async function getInviteCodes(): Promise { + const client = getClient(redis.utils.Databases.INVITATIONS) + const invites: { key: string; value: Invite }[] = await client.scan() - const results = invites.map(invite => { + const results: InviteWithCode[] = invites.map(invite => { return { ...invite.value, code: invite.key, From a6a75b533c0ea587516200691d75afb3b33c387c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:15:44 +0000 Subject: [PATCH 02/11] Reject inviting the same user twice. --- .../worker/src/api/controllers/global/users.ts | 4 +--- .../src/api/routes/global/tests/users.spec.ts | 2 ++ packages/worker/src/sdk/users/users.ts | 17 +++++++++++++---- packages/worker/src/utilities/redis.ts | 6 +++++- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 1f4168c00b..fae42acdfe 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -349,14 +349,12 @@ 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.body = await getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } - ctx.body = invites } export const updateInvite = async (ctx: any) => { diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index adcbca9d29..09cd4d358f 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -59,6 +59,8 @@ describe("/api/global/users", () => { const email = structures.users.newEmail() await config.api.users.sendUserInvite(sendMailMock, email) + jest.clearAllMocks() + const { code, res } = await config.api.users.sendUserInvite( sendMailMock, email, diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 8f04bb1941..4cca0b8fa6 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -1,5 +1,9 @@ import { events, tenancy, users as usersCore } from "@budibase/backend-core" -import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types" +import { + InviteUserRequest, + InviteUsersRequest, + InviteUsersResponse, +} from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" import { getInviteCodes } from "../..//utilities/redis" @@ -15,12 +19,17 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) - const existingInvites = await getInviteCodes() - const newUsers = [] + const invitedEmails = (await getInviteCodes()).map(invite => invite.email) + const newUsers: InviteUserRequest[] = [] // separate duplicates from new users for (let user of users) { - if (matchedEmails.includes(user.email)) { + if ( + matchedEmails.includes(user.email) || + invitedEmails.includes(user.email) + ) { + // This "Unavailable" is load bearing. The tests and frontend both check for it + // specifically response.unsuccessful.push({ email: user.email, reason: "Unavailable" }) } else { newUsers.push(user) diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 9852fb0467..337d34e376 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -66,7 +66,11 @@ async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { return code } -async function updateCode(db: RedisDBName, code: string, value: any) { +async function updateCode( + db: RedisDBName, + code: string, + value: Invite | PasswordReset +) { const client = getClient(db) await client.store(code, value, getExpirySecondsForDB(db)) } From b2841b30b23270139cd9560b9520758d78671f44 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 11:17:30 +0000 Subject: [PATCH 03/11] Add a test for the multi-invite endpoint. --- .../src/api/routes/global/tests/users.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 09cd4d358f..ce2c9347b4 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -120,6 +120,23 @@ describe("/api/global/users", () => { expect(sendMailMock).toHaveBeenCalledTimes(0) expect(events.user.invited).toBeCalledTimes(0) }) + + it("should not be able to generate an invitation for user that has already been invited", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + jest.clearAllMocks() + + const request = [{ email: email, userInfo: {} }] + const res = await config.api.users.sendMultiUserInvite(request) + + const body = res.body as InviteUsersResponse + expect(body.successful.length).toBe(0) + expect(body.unsuccessful.length).toBe(1) + expect(body.unsuccessful[0].reason).toBe("Unavailable") + expect(sendMailMock).toHaveBeenCalledTimes(0) + expect(events.user.invited).toBeCalledTimes(0) + }) }) describe("POST /api/global/users/bulk", () => { From b29cfc600cf628fae8b4740bdabc530cbb8f2e21 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 14:51:07 +0000 Subject: [PATCH 04/11] Move Invite and PasswordReset code into backend-core. --- packages/backend-core/src/redis/index.ts | 2 + packages/backend-core/src/redis/invite.ts | 96 ++++++++++ .../backend-core/src/redis/passwordReset.ts | 51 ++++++ packages/backend-core/src/users/lookup.ts | 4 + .../src/api/controllers/global/users.ts | 2 +- packages/worker/src/index.ts | 3 - packages/worker/src/sdk/auth/auth.ts | 4 +- packages/worker/src/sdk/users/users.ts | 7 +- packages/worker/src/utilities/email.ts | 3 +- packages/worker/src/utilities/redis.ts | 172 ------------------ 10 files changed, 159 insertions(+), 185 deletions(-) create mode 100644 packages/backend-core/src/redis/invite.ts create mode 100644 packages/backend-core/src/redis/passwordReset.ts delete mode 100644 packages/worker/src/utilities/redis.ts diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 6585d6e4fa..8d153ea5a1 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,3 +4,5 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" +export * as invite from "./invite" +export * as passwordReset from "./passwordReset" diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts new file mode 100644 index 0000000000..db36d3dfa6 --- /dev/null +++ b/packages/backend-core/src/redis/invite.ts @@ -0,0 +1,96 @@ +import { redis, utils, tenancy } from "../" +import env from "../environment" + +const TTL_SECONDS = 60 * 60 * 24 * 7 + +interface Invite { + email: string + info: any +} + +interface InviteWithCode extends Invite { + code: string +} + +let client: redis.Client + +async function getClient(): Promise { + if (!client) { + client = new redis.Client(redis.utils.Databases.INVITATIONS) + await client.init() + } + return client +} + +/** + * Given an invite code and invite body, allow the update an existing/valid invite in redis + * @param inviteCode The invite code for an invite in redis + * @param value The body of the updated user invitation + */ +export async function updateInviteCode(code: string, value: Invite) { + const client = await getClient() + await client.store(code, value, TTL_SECONDS) +} + +/** + * Generates an invitation code and writes it to redis - which can later be checked for user creation. + * @param email the email address which the code is being sent to (for use later). + * @param info Information to be carried along with the invitation. + * @return returns the code that was stored to redis. + */ +export async function createInviteCode( + email: string, + info: any +): Promise { + const client = await getClient() + const code = utils.newid() + await client.store(code, { email, info }, TTL_SECONDS) + return code +} + +/** + * Checks that the provided invite code is valid - will return the email address of user that was invited. + * @param inviteCode the invite code that was provided as part of the link. + * @return If the code is valid then an email address will be returned. + */ +export async function getInviteCode(code: string): Promise { + const client = await getClient() + const value = (await client.get(code)) as Invite | undefined + if (!value) { + throw "Invitation is not valid or has expired, please request a new one." + } + return value +} + +export async function deleteInviteCode(code: string) { + const client = await getClient() + await client.delete(code) +} + +/** + Get all currently available user invitations for the current tenant. +**/ +export async function getInviteCodes(): Promise { + const client = await getClient() + const invites: { key: string; value: Invite }[] = await client.scan() + + const results: InviteWithCode[] = invites.map(invite => { + return { + ...invite.value, + code: invite.key, + } + }) + if (!env.MULTI_TENANCY) { + return results + } + const tenantId = tenancy.getTenantId() + return results.filter(invite => tenantId === invite.info.tenantId) +} + +export async function getExistingInvites( + emails: string[] +): Promise { + return (await getInviteCodes()).filter(invite => + emails.includes(invite.email) + ) +} diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts new file mode 100644 index 0000000000..63c3371bba --- /dev/null +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -0,0 +1,51 @@ +import { redis, utils } from "../" + +const TTL_SECONDS = 60 * 60 + +interface PasswordReset { + userId: string + info: any +} + +let client: redis.Client + +async function getClient(): Promise { + if (!client) { + client = new redis.Client(redis.utils.Databases.PW_RESETS) + await client.init() + } + return client +} + +/** + * Given a user ID this will store a code (that is returned) for an hour in redis. + * The user can then return this code for resetting their password (through their reset link). + * @param userId the ID of the user which is to be reset. + * @param info Info about the user/the reset process. + * @return returns the code that was stored to redis. + */ +export async function createResetPasswordCode( + userId: string, + info: any +): Promise { + const client = await getClient() + const code = utils.newid() + await client.store(code, { userId, info }, TTL_SECONDS) + return code +} + +/** + * Given a reset code this will lookup to redis, check if the code is valid. + * @param code The code provided via the email link. + * @return returns the user ID if it is found + */ +export async function getResetPasswordCode( + code: string +): Promise { + const client = await getClient() + const value = (await client.get(code)) as PasswordReset | undefined + if (!value) { + throw "Provided information is not valid, cannot reset password - please try again." + } + return value +} diff --git a/packages/backend-core/src/users/lookup.ts b/packages/backend-core/src/users/lookup.ts index 17d0e91d88..2c0c66276a 100644 --- a/packages/backend-core/src/users/lookup.ts +++ b/packages/backend-core/src/users/lookup.ts @@ -6,6 +6,7 @@ import { } from "@budibase/types" import * as dbUtils from "../db" import { ViewName } from "../constants" +import { getExistingInvites } from "../redis/invite" /** * Apply a system-wide search on emails: @@ -26,6 +27,9 @@ export async function searchExistingEmails(emails: string[]) { const existingAccounts = await getExistingAccounts(emails) matchedEmails.push(...existingAccounts.map(account => account.email)) + const invitedEmails = await getExistingInvites(emails) + matchedEmails.push(...invitedEmails.map(invite => invite.email)) + return [...new Set(matchedEmails.map(email => email.toLowerCase()))] } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index fae42acdfe..8fb2b5675d 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -3,7 +3,7 @@ import { deleteInviteCode, getInviteCodes, updateInviteCode, -} from "../../../utilities/redis" +} from "@budibase/backend-core/src/redis/invite" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 4b1d11ecf7..1c101983df 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -22,7 +22,6 @@ import Koa from "koa" import koaBody from "koa-body" import http from "http" import api from "./api" -import * as redis from "./utilities/redis" const koaSession = require("koa-session") import { userAgent } from "koa-useragent" @@ -72,7 +71,6 @@ server.on("close", async () => { shuttingDown = true console.log("Server Closed") timers.cleanup() - await redis.shutdown() await events.shutdown() await queue.shutdown() if (!env.isTest()) { @@ -88,7 +86,6 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) await initPro() - await redis.init() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues await events.processors.init(proSdk.auditLogs.write) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index e25d34fa5e..57131294b3 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -6,12 +6,12 @@ import { sessions, tenancy, utils as coreUtils, + redis, } from "@budibase/backend-core" import { PlatformLogoutOpts, User } from "@budibase/types" import jwt from "jsonwebtoken" import * as userSdk from "../users" import * as emails from "../../utilities/email" -import * as redis from "../../utilities/redis" import { EmailTemplatePurpose } from "../../constants" // LOGIN / LOGOUT @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.getResetPasswordCode(resetCode) + const { userId } = await redis.passwordReset.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 4cca0b8fa6..9dc1bac0e9 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -6,7 +6,6 @@ import { } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" -import { getInviteCodes } from "../..//utilities/redis" export async function invite( users: InviteUsersRequest @@ -19,15 +18,11 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) - const invitedEmails = (await getInviteCodes()).map(invite => invite.email) const newUsers: InviteUserRequest[] = [] // separate duplicates from new users for (let user of users) { - if ( - matchedEmails.includes(user.email) || - invitedEmails.includes(user.email) - ) { + if (matchedEmails.includes(user.email)) { // This "Unavailable" is load bearing. The tests and frontend both check for it // specifically response.unsuccessful.push({ email: user.email, reason: "Unavailable" }) diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index a0c02d335c..f3f1fd40c4 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,8 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { createResetPasswordCode, createInviteCode } from "./redis" +import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" +import { createInviteCode } from "@budibase/backend-core/src/redis/invite" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts deleted file mode 100644 index 337d34e376..0000000000 --- a/packages/worker/src/utilities/redis.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { redis, utils, tenancy } from "@budibase/backend-core" -import env from "../environment" - -interface Invite { - email: string - info: any -} - -interface InviteWithCode extends Invite { - code: string -} - -interface PasswordReset { - userId: string - info: any -} - -type RedisDBName = - | redis.utils.Databases.PW_RESETS - | redis.utils.Databases.INVITATIONS -let pwResetClient: redis.Client, invitationClient: redis.Client - -export async function init() { - pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS) - invitationClient = new redis.Client(redis.utils.Databases.INVITATIONS) - await pwResetClient.init() - await invitationClient.init() -} - -export async function shutdown() { - if (pwResetClient) await pwResetClient.finish() - if (invitationClient) await invitationClient.finish() - // shutdown core clients - await redis.clients.shutdown() - console.log("Redis shutdown") -} - -function getExpirySecondsForDB(db: RedisDBName) { - switch (db) { - case redis.utils.Databases.PW_RESETS: - // a hour - return 3600 - case redis.utils.Databases.INVITATIONS: - // a week - return 604800 - default: - throw new Error(`Unknown redis database: ${db}`) - } -} - -function getClient(db: RedisDBName): redis.Client { - switch (db) { - case redis.utils.Databases.PW_RESETS: - return pwResetClient - case redis.utils.Databases.INVITATIONS: - return invitationClient - default: - throw new Error(`Unknown redis database: ${db}`) - } -} - -async function writeCode(db: RedisDBName, value: Invite | PasswordReset) { - const client = getClient(db) - const code = utils.newid() - await client.store(code, value, getExpirySecondsForDB(db)) - return code -} - -async function updateCode( - db: RedisDBName, - code: string, - value: Invite | PasswordReset -) { - const client = 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 inviteCode The invite code for an invite in redis - * @param value The body of the updated user invitation - */ -export async function updateInviteCode(code: string, value: Invite) { - await updateCode(redis.utils.Databases.INVITATIONS, code, value) -} - -async function deleteCode(db: RedisDBName, code: string) { - const client = getClient(db) - await client.delete(code) -} - -async function getCode(db: RedisDBName, code: string) { - const client = getClient(db) - const value = await client.get(code) - if (!value) { - throw new Error(`Could not find code: ${code}`) - } - return value -} - -/** - * Given a user ID this will store a code (that is returned) for an hour in redis. - * The user can then return this code for resetting their password (through their reset link). - * @param userId the ID of the user which is to be reset. - * @param info Info about the user/the reset process. - * @return returns the code that was stored to redis. - */ -export async function createResetPasswordCode(userId: string, info: any) { - return writeCode(redis.utils.Databases.PW_RESETS, { userId, info }) -} - -/** - * Given a reset code this will lookup to redis, check if the code is valid. - * @param resetCode The code provided via the email link. - * @return returns the user ID if it is found - */ -export async function getResetPasswordCode( - code: string -): Promise { - try { - return getCode(redis.utils.Databases.PW_RESETS, code) - } catch (err) { - throw "Provided information is not valid, cannot reset password - please try again." - } -} - -/** - * Generates an invitation code and writes it to redis - which can later be checked for user creation. - * @param email the email address which the code is being sent to (for use later). - * @param info Information to be carried along with the invitation. - * @return returns the code that was stored to redis. - */ -export async function createInviteCode(email: string, info: any) { - return writeCode(redis.utils.Databases.INVITATIONS, { email, info }) -} - -/** - * Checks that the provided invite code is valid - will return the email address of user that was invited. - * @param inviteCode the invite code that was provided as part of the link. - * @return If the code is valid then an email address will be returned. - */ -export async function getInviteCode(code: string): Promise { - try { - return getCode(redis.utils.Databases.INVITATIONS, code) - } catch (err) { - throw "Invitation is not valid or has expired, please request a new one." - } -} - -export async function deleteInviteCode(code: string) { - return deleteCode(redis.utils.Databases.INVITATIONS, code) -} - -/** - Get all currently available user invitations for the current tenant. -**/ -export async function getInviteCodes(): Promise { - const client = getClient(redis.utils.Databases.INVITATIONS) - const invites: { key: string; value: Invite }[] = await client.scan() - - const results: InviteWithCode[] = invites.map(invite => { - return { - ...invite.value, - code: invite.key, - } - }) - if (!env.MULTI_TENANCY) { - return results - } - const tenantId = tenancy.getTenantId() - return results.filter(invite => tenantId === invite.info.tenantId) -} From 822c03b0ef2d2bbbba996831a4c5cedf19e0d0bc Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 15:02:44 +0000 Subject: [PATCH 05/11] Refactor onboardUsers endpoint. --- packages/types/src/api/web/user.ts | 1 + .../src/api/controllers/global/users.ts | 79 ++++++------------- 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 3a5bd16bdf..6db70f20d0 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -10,6 +10,7 @@ export interface SaveUserResponse { export interface UserDetails { _id: string email: string + password?: string } export interface BulkUserRequest { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 8fb2b5675d..6608fcde05 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -251,58 +251,32 @@ export const tenantUserLookup = async (ctx: any) => { Encapsulate the app user onboarding flows here. */ export const onboardUsers = async (ctx: Ctx) => { - const request = ctx.request.body - 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 userSdk.db.bulkCreate(assignUsers, groups) - ctx.body = onboardingResponse - } else if (emailConfigured) { - onboardingResponse = await inviteMultiple(ctx) - } else if (!emailConfigured) { - const inviteRequest = ctx.request.body - - 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, - forceResetPassword: true, - roles: invite.userInfo.apps, - admin: invite.userInfo.admin, - builder: invite.userInfo.builder, - tenantId: tenancy.getTenantId(), - } - }) - let bulkCreateReponse = await userSdk.db.bulkCreate(users, []) - - // Apply temporary credentials - ctx.body = { - ...bulkCreateReponse, - successful: bulkCreateReponse?.successful.map(user => { - return { - ...user, - password: createdPasswords[user.email], - } - }), - created: true, - } - } else { - ctx.throw(400, "User onboarding failed") + if (await isEmailConfigured()) { + await inviteMultiple(ctx) + return } + + let createdPasswords: Record = {} + const users: User[] = ctx.request.body.map(invite => { + let password = Math.random().toString(36).substring(2, 22) + createdPasswords[invite.email] = password + + return { + email: invite.email, + password, + forceResetPassword: true, + roles: invite.userInfo.apps, + admin: invite.userInfo.admin, + builder: invite.userInfo.builder, + tenantId: tenancy.getTenantId(), + } + }) + + let resp = await userSdk.db.bulkCreate(users, []) + resp.successful.forEach(user => { + user.password = createdPasswords[user.email] + }) + ctx.body = { ...resp, created: true } } export const invite = async (ctx: Ctx) => { @@ -329,8 +303,7 @@ export const invite = async (ctx: Ctx) => { } export const inviteMultiple = async (ctx: Ctx) => { - const request = ctx.request.body - ctx.body = await userSdk.invite(request) + ctx.body = await userSdk.invite(ctx.request.body) } export const checkInvite = async (ctx: any) => { From 7f530eeab5970feda052576ad3c786b1f25673cf Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 9 Nov 2023 15:13:59 +0000 Subject: [PATCH 06/11] Add tests for the onboarding endpoint. --- packages/backend-core/src/users/db.ts | 4 ++-- packages/types/src/api/web/user.ts | 1 + .../src/api/controllers/global/users.ts | 11 ++++++---- .../src/api/routes/global/tests/users.spec.ts | 21 +++++++++++++++++++ packages/worker/src/tests/api/users.ts | 21 +++++++++++++++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 59f698d99c..bd85097bbd 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -303,7 +303,7 @@ export class UserDB { static async bulkCreate( newUsersRequested: User[], - groups: string[] + groups?: string[] ): Promise { const tenantId = getTenantId() @@ -328,7 +328,7 @@ export class UserDB { }) continue } - newUser.userGroups = groups + newUser.userGroups = groups || [] newUsers.push(newUser) if (isCreator(newUser)) { newCreators.push(newUser) diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 6db70f20d0..0de42622e6 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -50,6 +50,7 @@ export type InviteUsersRequest = InviteUserRequest[] export interface InviteUsersResponse { successful: { email: string }[] unsuccessful: { email: string; reason: string }[] + created?: boolean } export interface SearchUsersRequest { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 6608fcde05..e904567674 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -17,6 +17,7 @@ import { Ctx, InviteUserRequest, InviteUsersRequest, + InviteUsersResponse, MigrationType, SaveUserResponse, SearchUsersRequest, @@ -250,7 +251,9 @@ export const tenantUserLookup = async (ctx: any) => { /* Encapsulate the app user onboarding flows here. */ -export const onboardUsers = async (ctx: Ctx) => { +export const onboardUsers = async ( + ctx: Ctx +) => { if (await isEmailConfigured()) { await inviteMultiple(ctx) return @@ -272,10 +275,10 @@ export const onboardUsers = async (ctx: Ctx) => { } }) - let resp = await userSdk.db.bulkCreate(users, []) - resp.successful.forEach(user => { + let resp = await userSdk.db.bulkCreate(users) + for (const user of resp.successful) { user.password = createdPasswords[user.email] - }) + } ctx.body = { ...resp, created: true } } diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index ce2c9347b4..a85933255a 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -669,4 +669,25 @@ describe("/api/global/users", () => { expect(response.body.message).toBe("Unable to delete self.") }) }) + + describe("POST /api/global/users/onboard", () => { + it("should successfully onboard a user", async () => { + const response = await config.api.users.onboardUser([ + { email: structures.users.newEmail(), userInfo: {} }, + ]) + expect(response.successful.length).toBe(1) + expect(response.unsuccessful.length).toBe(0) + }) + + it("should not onboard a user who has been invited", async () => { + const email = structures.users.newEmail() + await config.api.users.sendUserInvite(sendMailMock, email) + + const response = await config.api.users.onboardUser([ + { email, userInfo: {} }, + ]) + expect(response.successful.length).toBe(0) + expect(response.unsuccessful.length).toBe(1) + }) + }) }) diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index ca25e2f9ca..5ecd1447ca 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -5,6 +5,7 @@ import { User, CreateAdminUserRequest, SearchQuery, + InviteUsersResponse, } from "@budibase/types" import structures from "../structures" import { generator } from "@budibase/backend-core/tests" @@ -176,4 +177,24 @@ export class UserAPI extends TestAPI { .expect("Content-Type", /json/) .expect(200) } + + onboardUser = async ( + req: InviteUsersRequest + ): Promise => { + const resp = await this.request + .post(`/api/global/users/onboard`) + .send(req) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + + if (resp.status !== 200) { + throw new Error( + `request failed with status ${resp.status} and body ${JSON.stringify( + resp.body + )}` + ) + } + + return resp.body as InviteUsersResponse + } } From d98e217c6c1ba90ccddfc9833609e145ed6785f5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:21:36 +0000 Subject: [PATCH 07/11] Fix backend-core redis imports. --- packages/backend-core/src/redis/index.ts | 5 +++-- .../src/api/controllers/global/users.ts | 19 +++++++------------ packages/worker/src/sdk/auth/auth.ts | 2 +- packages/worker/src/utilities/email.ts | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 8d153ea5a1..419f1db700 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,5 +4,6 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" -export * as invite from "./invite" -export * as passwordReset from "./passwordReset" + +export * from "./invite" +export * from "./passwordReset" diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index e904567674..cd11fc74b6 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,9 +1,4 @@ -import { - getInviteCode, - deleteInviteCode, - getInviteCodes, - updateInviteCode, -} from "@budibase/backend-core/src/redis/invite" +import { redis } from "@budibase/backend-core" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { @@ -313,7 +308,7 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await getInviteCode(code) + invite = await redis.getInviteCode(code) } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") @@ -327,7 +322,7 @@ export const checkInvite = async (ctx: any) => { export const getUserInvites = async (ctx: any) => { try { // Restricted to the currently authenticated tenant - ctx.body = await getInviteCodes() + ctx.body = await redis.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } @@ -341,7 +336,7 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await getInviteCode(code) + invite = await redis.getInviteCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") return @@ -369,7 +364,7 @@ export const updateInvite = async (ctx: any) => { } } - await updateInviteCode(code, updated) + await redis.updateInviteCode(code, updated) ctx.body = { ...invite } } @@ -379,8 +374,8 @@ export const inviteAccept = async ( 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 getInviteCode(inviteCode) - await deleteInviteCode(inviteCode) + const { email, info }: any = await redis.getInviteCode(inviteCode) + await redis.deleteInviteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 57131294b3..2140b89ce3 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.passwordReset.getResetPasswordCode(resetCode) + const { userId } = await redis.getResetPasswordCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index f3f1fd40c4..a4d2c296e5 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -4,7 +4,7 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" -import { createInviteCode } from "@budibase/backend-core/src/redis/invite" +import { redis } from "@budibase/backend-core" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" import ical from "ical-generator" @@ -64,7 +64,7 @@ async function getLinkCode( case EmailTemplatePurpose.PASSWORD_RECOVERY: return createResetPasswordCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return createInviteCode(email, info) + return redis.createInviteCode(email, info) default: return null } From dd2f68d0990b7019ac0faa8caf4c74f09dcae07b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:24:55 +0000 Subject: [PATCH 08/11] Hook new Redis clients into init/shutdown flow. --- packages/backend-core/src/redis/init.ts | 22 +++++++++++++++++- packages/backend-core/src/redis/invite.ts | 23 ++++++------------- .../backend-core/src/redis/passwordReset.ts | 17 ++++---------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 55ffe3dd12..a4f1fecc17 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -7,7 +7,9 @@ let userClient: Client, cacheClient: Client, writethroughClient: Client, lockClient: Client, - socketClient: Client + socketClient: Client, + inviteClient: Client, + passwordResetClient: Client async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() @@ -20,6 +22,8 @@ async function init() { utils.Databases.SOCKET_IO, utils.SelectableDatabase.SOCKET_IO ).init() + inviteClient = await new Client(utils.Databases.INVITATIONS).init() + passwordResetClient = await new Client(utils.Databases.PW_RESETS).init() } export async function shutdown() { @@ -30,6 +34,8 @@ export async function shutdown() { if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() if (socketClient) await socketClient.finish() + if (inviteClient) await inviteClient.finish() + if (passwordResetClient) await passwordResetClient.finish() } process.on("exit", async () => { @@ -84,3 +90,17 @@ export async function getSocketClient() { } return socketClient } + +export async function getInviteClient() { + if (!inviteClient) { + await init() + } + return inviteClient +} + +export async function getPasswordResetClient() { + if (!passwordResetClient) { + await init() + } + return passwordResetClient +} diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts index db36d3dfa6..6e6a1fd9e9 100644 --- a/packages/backend-core/src/redis/invite.ts +++ b/packages/backend-core/src/redis/invite.ts @@ -1,5 +1,6 @@ -import { redis, utils, tenancy } from "../" +import { utils, tenancy } from "../" import env from "../environment" +import { getInviteClient } from "./init" const TTL_SECONDS = 60 * 60 * 24 * 7 @@ -12,23 +13,13 @@ interface InviteWithCode extends Invite { code: string } -let client: redis.Client - -async function getClient(): Promise { - if (!client) { - client = new redis.Client(redis.utils.Databases.INVITATIONS) - await client.init() - } - return client -} - /** * Given an invite code and invite body, allow the update an existing/valid invite in redis * @param inviteCode The invite code for an invite in redis * @param value The body of the updated user invitation */ export async function updateInviteCode(code: string, value: Invite) { - const client = await getClient() + const client = await getInviteClient() await client.store(code, value, TTL_SECONDS) } @@ -42,7 +33,7 @@ export async function createInviteCode( email: string, info: any ): Promise { - const client = await getClient() + const client = await getInviteClient() const code = utils.newid() await client.store(code, { email, info }, TTL_SECONDS) return code @@ -54,7 +45,7 @@ export async function createInviteCode( * @return If the code is valid then an email address will be returned. */ export async function getInviteCode(code: string): Promise { - const client = await getClient() + const client = await getInviteClient() const value = (await client.get(code)) as Invite | undefined if (!value) { throw "Invitation is not valid or has expired, please request a new one." @@ -63,7 +54,7 @@ export async function getInviteCode(code: string): Promise { } export async function deleteInviteCode(code: string) { - const client = await getClient() + const client = await getInviteClient() await client.delete(code) } @@ -71,7 +62,7 @@ export async function deleteInviteCode(code: string) { Get all currently available user invitations for the current tenant. **/ export async function getInviteCodes(): Promise { - const client = await getClient() + const client = await getInviteClient() const invites: { key: string; value: Invite }[] = await client.scan() const results: InviteWithCode[] = invites.map(invite => { diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts index 63c3371bba..243b73c529 100644 --- a/packages/backend-core/src/redis/passwordReset.ts +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -1,4 +1,5 @@ -import { redis, utils } from "../" +import { utils } from "../" +import { getPasswordResetClient } from "./init" const TTL_SECONDS = 60 * 60 @@ -7,16 +8,6 @@ interface PasswordReset { info: any } -let client: redis.Client - -async function getClient(): Promise { - if (!client) { - client = new redis.Client(redis.utils.Databases.PW_RESETS) - await client.init() - } - return client -} - /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -28,7 +19,7 @@ export async function createResetPasswordCode( userId: string, info: any ): Promise { - const client = await getClient() + const client = await getPasswordResetClient() const code = utils.newid() await client.store(code, { userId, info }, TTL_SECONDS) return code @@ -42,7 +33,7 @@ export async function createResetPasswordCode( export async function getResetPasswordCode( code: string ): Promise { - const client = await getClient() + const client = await getPasswordResetClient() const value = (await client.get(code)) as PasswordReset | undefined if (!value) { throw "Provided information is not valid, cannot reset password - please try again." From 94983c289fc578033e2d20466388adf05858dfa3 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 11:39:26 +0000 Subject: [PATCH 09/11] Hook redis init flow into overall worker init flow. --- packages/backend-core/src/index.ts | 5 ++++- packages/backend-core/src/redis/index.ts | 1 + packages/backend-core/src/redis/init.ts | 2 +- packages/worker/src/db/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index c7cf9f56cc..d41f2b9384 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -37,6 +37,7 @@ export { SearchParams } from "./db" // circular dependencies import * as context from "./context" import * as _tenancy from "./tenancy" +import * as redis from "./redis" export const tenancy = { ..._tenancy, ...context, @@ -50,6 +51,8 @@ export * from "./constants" // expose package init function import * as db from "./db" -export const init = (opts: any = {}) => { + +export const init = async (opts: any = {}) => { db.init(opts.db) + await redis.init() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 419f1db700..cc9eb854b4 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,6 +4,7 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" +export * from "./init" export * from "./invite" export * from "./passwordReset" diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index a4f1fecc17..8f2d2914b5 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -11,7 +11,7 @@ let userClient: Client, inviteClient: Client, passwordResetClient: Client -async function init() { +export async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() diff --git a/packages/worker/src/db/index.ts b/packages/worker/src/db/index.ts index 157c2f4fb3..19f8f8acee 100644 --- a/packages/worker/src/db/index.ts +++ b/packages/worker/src/db/index.ts @@ -1,7 +1,7 @@ import * as core from "@budibase/backend-core" import env from "../environment" -export function init() { +export async function init() { const dbConfig: any = { replication: true, find: true, @@ -12,5 +12,5 @@ export function init() { dbConfig.allDbs = true } - core.init({ db: dbConfig }) + await core.init({ db: dbConfig }) } From d6eb2b945223c5ef72113b2db87d30d795c02a3b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 15:43:06 +0000 Subject: [PATCH 10/11] Attempting to get integration tests passing again. --- packages/worker/src/index.ts | 3 +- .../worker/src/tests/TestConfiguration.ts | 2 +- qa-core/yarn.lock | 28 +++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 1c101983df..d40c7f0668 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -17,7 +17,7 @@ import { env as coreEnv, timers, } from "@budibase/backend-core" -db.init() + import Koa from "koa" import koaBody from "koa-body" import http from "http" @@ -85,6 +85,7 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) + await db.init() await initPro() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index d4fcbeebd6..1961d22c34 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -7,7 +7,6 @@ mocks.licenses.init(mocks.pro) mocks.licenses.useUnlimited() import * as dbConfig from "../db" -dbConfig.init() import env from "../environment" import * as controllers from "./controllers" const supertest = require("supertest") @@ -109,6 +108,7 @@ class TestConfiguration { // SETUP / TEARDOWN async beforeAll() { + await dbConfig.init() try { await this.createDefaultUser() await this.createSession(this.user!) diff --git a/qa-core/yarn.lock b/qa-core/yarn.lock index d2cde27530..1de9d75e60 100644 --- a/qa-core/yarn.lock +++ b/qa-core/yarn.lock @@ -983,10 +983,10 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/node-fetch@2.6.2": - version "2.6.2" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== +"@types/node-fetch@2.6.4": + version "2.6.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" + integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== dependencies: "@types/node" "*" form-data "^3.0.0" @@ -3587,18 +3587,18 @@ node-duration@^1.0.4: resolved "https://registry.npmjs.org/node-duration/-/node-duration-1.0.4.tgz" integrity sha512-eUXYNSY7DL53vqfTosggWkvyIW3bhAcqBDIlolgNYlZhianXTrCL50rlUJWD1eRqkIxMppXTfiFbp+9SjpPrgA== -node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.7: +node-fetch@2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-fetch@2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== - node-gyp-build-optional-packages@5.0.7: version "5.0.7" resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz" @@ -4893,10 +4893,10 @@ type-is@^1.6.16, type-is@^1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@4.7.3: - version "4.7.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz" - integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== +typescript@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== uid2@0.0.x: version "0.0.4" From 4c7c10b121682ad53acbd8ae3aec1765d08060c9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 10 Nov 2023 16:17:18 +0000 Subject: [PATCH 11/11] Set Redis initialisation back to how it was before I started messing with it. --- packages/backend-core/src/index.ts | 4 +-- packages/backend-core/src/redis/index.ts | 6 ++-- packages/backend-core/src/redis/init.ts | 24 ++------------ packages/backend-core/src/redis/invite.ts | 32 +++++++++++-------- .../backend-core/src/redis/passwordReset.ts | 27 +++++++++------- .../src/api/controllers/global/users.ts | 12 +++---- packages/worker/src/db/index.ts | 4 +-- packages/worker/src/index.ts | 8 +++-- packages/worker/src/sdk/auth/auth.ts | 2 +- .../worker/src/tests/TestConfiguration.ts | 2 +- packages/worker/src/utilities/email.ts | 5 ++- 11 files changed, 57 insertions(+), 69 deletions(-) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index d41f2b9384..f6c091d679 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -37,7 +37,6 @@ export { SearchParams } from "./db" // circular dependencies import * as context from "./context" import * as _tenancy from "./tenancy" -import * as redis from "./redis" export const tenancy = { ..._tenancy, ...context, @@ -52,7 +51,6 @@ export * from "./constants" // expose package init function import * as db from "./db" -export const init = async (opts: any = {}) => { +export const init = (opts: any = {}) => { db.init(opts.db) - await redis.init() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index cc9eb854b4..8d153ea5a1 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,7 +4,5 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" export * as locks from "./redlockImpl" -export * from "./init" - -export * from "./invite" -export * from "./passwordReset" +export * as invite from "./invite" +export * as passwordReset from "./passwordReset" diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 8f2d2914b5..55ffe3dd12 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -7,11 +7,9 @@ let userClient: Client, cacheClient: Client, writethroughClient: Client, lockClient: Client, - socketClient: Client, - inviteClient: Client, - passwordResetClient: Client + socketClient: Client -export async function init() { +async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() @@ -22,8 +20,6 @@ export async function init() { utils.Databases.SOCKET_IO, utils.SelectableDatabase.SOCKET_IO ).init() - inviteClient = await new Client(utils.Databases.INVITATIONS).init() - passwordResetClient = await new Client(utils.Databases.PW_RESETS).init() } export async function shutdown() { @@ -34,8 +30,6 @@ export async function shutdown() { if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() if (socketClient) await socketClient.finish() - if (inviteClient) await inviteClient.finish() - if (passwordResetClient) await passwordResetClient.finish() } process.on("exit", async () => { @@ -90,17 +84,3 @@ export async function getSocketClient() { } return socketClient } - -export async function getInviteClient() { - if (!inviteClient) { - await init() - } - return inviteClient -} - -export async function getPasswordResetClient() { - if (!passwordResetClient) { - await init() - } - return passwordResetClient -} diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/redis/invite.ts index 6e6a1fd9e9..006a06fe26 100644 --- a/packages/backend-core/src/redis/invite.ts +++ b/packages/backend-core/src/redis/invite.ts @@ -1,6 +1,5 @@ -import { utils, tenancy } from "../" +import { utils, tenancy, redis } from "../" import env from "../environment" -import { getInviteClient } from "./init" const TTL_SECONDS = 60 * 60 * 24 * 7 @@ -13,13 +12,25 @@ interface InviteWithCode extends Invite { code: string } +let client: redis.Client + +export async function init() { + if (!client) { + client = new redis.Client(redis.utils.Databases.INVITATIONS) + } + return client +} + +export async function shutdown() { + if (client) await client.finish() +} + /** * Given an invite code and invite body, allow the update an existing/valid invite in redis * @param inviteCode The invite code for an invite in redis * @param value The body of the updated user invitation */ -export async function updateInviteCode(code: string, value: Invite) { - const client = await getInviteClient() +export async function updateCode(code: string, value: Invite) { await client.store(code, value, TTL_SECONDS) } @@ -29,11 +40,7 @@ export async function updateInviteCode(code: string, value: Invite) { * @param info Information to be carried along with the invitation. * @return returns the code that was stored to redis. */ -export async function createInviteCode( - email: string, - info: any -): Promise { - const client = await getInviteClient() +export async function createCode(email: string, info: any): Promise { const code = utils.newid() await client.store(code, { email, info }, TTL_SECONDS) return code @@ -44,8 +51,7 @@ export async function createInviteCode( * @param inviteCode the invite code that was provided as part of the link. * @return If the code is valid then an email address will be returned. */ -export async function getInviteCode(code: string): Promise { - const client = await getInviteClient() +export async function getCode(code: string): Promise { const value = (await client.get(code)) as Invite | undefined if (!value) { throw "Invitation is not valid or has expired, please request a new one." @@ -53,8 +59,7 @@ export async function getInviteCode(code: string): Promise { return value } -export async function deleteInviteCode(code: string) { - const client = await getInviteClient() +export async function deleteCode(code: string) { await client.delete(code) } @@ -62,7 +67,6 @@ export async function deleteInviteCode(code: string) { Get all currently available user invitations for the current tenant. **/ export async function getInviteCodes(): Promise { - const client = await getInviteClient() const invites: { key: string; value: Invite }[] = await client.scan() const results: InviteWithCode[] = invites.map(invite => { diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/redis/passwordReset.ts index 243b73c529..13c1b1d2e6 100644 --- a/packages/backend-core/src/redis/passwordReset.ts +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -1,5 +1,4 @@ -import { utils } from "../" -import { getPasswordResetClient } from "./init" +import { redis, utils } from "../" const TTL_SECONDS = 60 * 60 @@ -8,6 +7,19 @@ interface PasswordReset { info: any } +let client: redis.Client + +export async function init() { + if (!client) { + client = new redis.Client(redis.utils.Databases.PW_RESETS) + } + return client +} + +export async function shutdown() { + if (client) await client.finish() +} + /** * Given a user ID this will store a code (that is returned) for an hour in redis. * The user can then return this code for resetting their password (through their reset link). @@ -15,11 +27,7 @@ interface PasswordReset { * @param info Info about the user/the reset process. * @return returns the code that was stored to redis. */ -export async function createResetPasswordCode( - userId: string, - info: any -): Promise { - const client = await getPasswordResetClient() +export async function createCode(userId: string, info: any): Promise { const code = utils.newid() await client.store(code, { userId, info }, TTL_SECONDS) return code @@ -30,10 +38,7 @@ export async function createResetPasswordCode( * @param code The code provided via the email link. * @return returns the user ID if it is found */ -export async function getResetPasswordCode( - code: string -): Promise { - const client = await getPasswordResetClient() +export async function getCode(code: string): Promise { const value = (await client.get(code)) as PasswordReset | undefined if (!value) { throw "Provided information is not valid, cannot reset password - please try again." diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index cd11fc74b6..affdaa9938 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -308,7 +308,7 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await redis.getInviteCode(code) + invite = await redis.invite.getCode(code) } catch (e) { console.warn("Error getting invite from code", e) ctx.throw(400, "There was a problem with the invite") @@ -322,7 +322,7 @@ export const checkInvite = async (ctx: any) => { export const getUserInvites = async (ctx: any) => { try { // Restricted to the currently authenticated tenant - ctx.body = await redis.getInviteCodes() + ctx.body = await redis.invite.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } @@ -336,7 +336,7 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await redis.getInviteCode(code) + invite = await redis.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") return @@ -364,7 +364,7 @@ export const updateInvite = async (ctx: any) => { } } - await redis.updateInviteCode(code, updated) + await redis.invite.updateCode(code, updated) ctx.body = { ...invite } } @@ -374,8 +374,8 @@ export const inviteAccept = async ( 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 redis.getInviteCode(inviteCode) - await redis.deleteInviteCode(inviteCode) + const { email, info }: any = await redis.invite.getCode(inviteCode) + await redis.invite.deleteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/db/index.ts b/packages/worker/src/db/index.ts index 19f8f8acee..157c2f4fb3 100644 --- a/packages/worker/src/db/index.ts +++ b/packages/worker/src/db/index.ts @@ -1,7 +1,7 @@ import * as core from "@budibase/backend-core" import env from "../environment" -export async function init() { +export function init() { const dbConfig: any = { replication: true, find: true, @@ -12,5 +12,5 @@ export async function init() { dbConfig.allDbs = true } - await core.init({ db: dbConfig }) + core.init({ db: dbConfig }) } diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index d40c7f0668..e486a67433 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -16,8 +16,9 @@ import { queue, env as coreEnv, timers, + redis, } from "@budibase/backend-core" - +db.init() import Koa from "koa" import koaBody from "koa-body" import http from "http" @@ -71,6 +72,8 @@ server.on("close", async () => { shuttingDown = true console.log("Server Closed") timers.cleanup() + await redis.invite.shutdown() + await redis.passwordReset.shutdown() await events.shutdown() await queue.shutdown() if (!env.isTest()) { @@ -85,8 +88,9 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) - await db.init() await initPro() + await redis.invite.init() + await redis.passwordReset.init() // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues await events.processors.init(proSdk.auditLogs.write) diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 2140b89ce3..704de9e4b2 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -73,7 +73,7 @@ export const reset = async (email: string) => { * Perform the user password update if the provided reset code is valid. */ export const resetUpdate = async (resetCode: string, password: string) => { - const { userId } = await redis.getResetPasswordCode(resetCode) + const { userId } = await redis.passwordReset.getCode(resetCode) let user = await userSdk.db.getUser(userId) user.password = password diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 1961d22c34..d4fcbeebd6 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -7,6 +7,7 @@ mocks.licenses.init(mocks.pro) mocks.licenses.useUnlimited() import * as dbConfig from "../db" +dbConfig.init() import env from "../environment" import * as controllers from "./controllers" const supertest = require("supertest") @@ -108,7 +109,6 @@ class TestConfiguration { // SETUP / TEARDOWN async beforeAll() { - await dbConfig.init() try { await this.createDefaultUser() await this.createSession(this.user!) diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index a4d2c296e5..530b6ce87f 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,7 +3,6 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { createResetPasswordCode } from "@budibase/backend-core/src/redis/passwordReset" import { redis } from "@budibase/backend-core" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" import { configs } from "@budibase/backend-core" @@ -62,9 +61,9 @@ async function getLinkCode( ) { switch (purpose) { case EmailTemplatePurpose.PASSWORD_RECOVERY: - return createResetPasswordCode(user._id!, info) + return redis.passwordReset.createCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return redis.createInviteCode(email, info) + return redis.invite.createCode(email, info) default: return null }