From 7fb6c0927a16f394dfdbe881298462f1c4ebd719 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 17 Nov 2023 16:20:10 +0000 Subject: [PATCH] Moving things around to get rid of cyclics created by moving invite/passwordReset into backend-core - also updating currentapp test case to mock a bit better. --- packages/backend-core/src/cache/generic.ts | 20 ++-- packages/backend-core/src/cache/index.ts | 2 + .../src/{redis => cache}/invite.ts | 37 +++---- .../src/{redis => cache}/passwordReset.ts | 21 ++-- packages/backend-core/src/redis/index.ts | 2 - packages/backend-core/src/redis/init.ts | 22 ++++- packages/backend-core/src/users/lookup.ts | 2 +- .../src/middleware/tests/currentapp.spec.js | 97 +++++-------------- .../src/api/controllers/global/users.ts | 13 ++- packages/worker/src/index.ts | 9 +- packages/worker/src/sdk/auth/auth.ts | 4 +- packages/worker/src/utilities/email.ts | 7 +- 12 files changed, 95 insertions(+), 141 deletions(-) rename packages/backend-core/src/{redis => cache}/invite.ts (74%) rename packages/backend-core/src/{redis => cache}/passwordReset.ts (77%) diff --git a/packages/backend-core/src/cache/generic.ts b/packages/backend-core/src/cache/generic.ts index 7cd5d6227f..19365e0e3f 100644 --- a/packages/backend-core/src/cache/generic.ts +++ b/packages/backend-core/src/cache/generic.ts @@ -1,6 +1,6 @@ -const BaseCache = require("./base") +import BaseCache from "./base" -const GENERIC = new BaseCache.default() +const GENERIC = new BaseCache() export enum CacheKey { CHECKLIST = "checklist", @@ -18,13 +18,9 @@ export enum TTL { ONE_DAY = 86400, } -function performExport(funcName: string) { - return (...args: any) => GENERIC[funcName](...args) -} - -export const keys = performExport("keys") -export const get = performExport("get") -export const store = performExport("store") -export const destroy = performExport("delete") -export const withCache = performExport("withCache") -export const bustCache = performExport("bustCache") +export const keys = GENERIC.keys +export const get = GENERIC.get +export const store = GENERIC.store +export const destroy = GENERIC.delete +export const withCache = GENERIC.withCache +export const bustCache = GENERIC.bustCache diff --git a/packages/backend-core/src/cache/index.ts b/packages/backend-core/src/cache/index.ts index 58928c271a..4fa986e4e2 100644 --- a/packages/backend-core/src/cache/index.ts +++ b/packages/backend-core/src/cache/index.ts @@ -2,4 +2,6 @@ export * as generic from "./generic" export * as user from "./user" export * as app from "./appMetadata" export * as writethrough from "./writethrough" +export * as invite from "./invite" +export * as passwordReset from "./passwordReset" export * from "./generic" diff --git a/packages/backend-core/src/redis/invite.ts b/packages/backend-core/src/cache/invite.ts similarity index 74% rename from packages/backend-core/src/redis/invite.ts rename to packages/backend-core/src/cache/invite.ts index 006a06fe26..1fec52122e 100644 --- a/packages/backend-core/src/redis/invite.ts +++ b/packages/backend-core/src/cache/invite.ts @@ -1,7 +1,10 @@ -import { utils, tenancy, redis } from "../" +import * as utils from "../utils" +import { Duration, DurationType } from "../utils" import env from "../environment" +import { getTenantId } from "../context" +import * as redis from "../redis/init" -const TTL_SECONDS = 60 * 60 * 24 * 7 +const TTL_SECONDS = Duration.fromDays(7).to(DurationType.SECONDS) interface Invite { email: string @@ -12,25 +15,13 @@ 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 code The invite code for an invite in redis * @param value The body of the updated user invitation */ export async function updateCode(code: string, value: Invite) { + const client = await redis.getInviteClient() await client.store(code, value, TTL_SECONDS) } @@ -42,16 +33,18 @@ export async function updateCode(code: string, value: Invite) { */ export async function createCode(email: string, info: any): Promise { const code = utils.newid() + const client = await redis.getInviteClient() 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. + * @param code 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 getCode(code: string): Promise { + const client = await redis.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." @@ -60,13 +53,15 @@ export async function getCode(code: string): Promise { } export async function deleteCode(code: string) { + const client = await redis.getInviteClient() await client.delete(code) } -/** - Get all currently available user invitations for the current tenant. -**/ +/** + Get all currently available user invitations for the current tenant. + **/ export async function getInviteCodes(): Promise { + const client = await redis.getInviteClient() const invites: { key: string; value: Invite }[] = await client.scan() const results: InviteWithCode[] = invites.map(invite => { @@ -78,7 +73,7 @@ export async function getInviteCodes(): Promise { if (!env.MULTI_TENANCY) { return results } - const tenantId = tenancy.getTenantId() + const tenantId = getTenantId() return results.filter(invite => tenantId === invite.info.tenantId) } diff --git a/packages/backend-core/src/redis/passwordReset.ts b/packages/backend-core/src/cache/passwordReset.ts similarity index 77% rename from packages/backend-core/src/redis/passwordReset.ts rename to packages/backend-core/src/cache/passwordReset.ts index 13c1b1d2e6..9ee50c634c 100644 --- a/packages/backend-core/src/redis/passwordReset.ts +++ b/packages/backend-core/src/cache/passwordReset.ts @@ -1,25 +1,14 @@ -import { redis, utils } from "../" +import * as redis from "../redis/init" +import * as utils from "../utils" +import { Duration, DurationType } from "../utils" -const TTL_SECONDS = 60 * 60 +const TTL_SECONDS = Duration.fromHours(1).to(DurationType.SECONDS) interface PasswordReset { userId: string 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). @@ -29,6 +18,7 @@ export async function shutdown() { */ export async function createCode(userId: string, info: any): Promise { const code = utils.newid() + const client = await redis.getPasswordResetClient() await client.store(code, { userId, info }, TTL_SECONDS) return code } @@ -39,6 +29,7 @@ export async function createCode(userId: string, info: any): Promise { * @return returns the user ID if it is found */ export async function getCode(code: string): Promise { + const client = await redis.getPasswordResetClient() 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/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 8d153ea5a1..6585d6e4fa 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -4,5 +4,3 @@ 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/init.ts b/packages/backend-core/src/redis/init.ts index 55ffe3dd12..9ec3246fac 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() @@ -16,6 +18,8 @@ async function init() { cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() lockClient = await new Client(utils.Databases.LOCKS).init() writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).init() + inviteClient = await new Client(utils.Databases.INVITATIONS).init() + passwordResetClient = await new Client(utils.Databases.PW_RESETS).init() socketClient = await new Client( utils.Databases.SOCKET_IO, utils.SelectableDatabase.SOCKET_IO @@ -29,6 +33,8 @@ export async function shutdown() { if (cacheClient) await cacheClient.finish() if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() + if (inviteClient) await inviteClient.finish() + if (passwordResetClient) await passwordResetClient.finish() if (socketClient) await socketClient.finish() } @@ -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/users/lookup.ts b/packages/backend-core/src/users/lookup.ts index 2c0c66276a..355be74dab 100644 --- a/packages/backend-core/src/users/lookup.ts +++ b/packages/backend-core/src/users/lookup.ts @@ -6,7 +6,7 @@ import { } from "@budibase/types" import * as dbUtils from "../db" import { ViewName } from "../constants" -import { getExistingInvites } from "../redis/invite" +import { getExistingInvites } from "../cache/invite" /** * Apply a system-wide search on emails: diff --git a/packages/server/src/middleware/tests/currentapp.spec.js b/packages/server/src/middleware/tests/currentapp.spec.js index b80800fd96..d287683869 100644 --- a/packages/server/src/middleware/tests/currentapp.spec.js +++ b/packages/server/src/middleware/tests/currentapp.spec.js @@ -9,11 +9,11 @@ function mockWorker() { return { _id: "us_uuid1", roles: { - "app_test": "BASIC", + app_test: "BASIC", }, roleId: "BASIC", } - } + }, })) } @@ -37,27 +37,12 @@ function mockAuthWithNoCookie() { mockWorker() jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - db: { - ...core.db, - dbExists: () => true, - }, - cache: { - user: { - getUser: async id => { - return { - _id: "us_uuid1", - } - }, - }, - }, - utils: { - getAppIdFromCtx: jest.fn(), - setCookie: jest.fn(), - getCookie: jest.fn(), - }, - } + core.db.dbExists = () => true + core.cache.user.getUser = () => ({ _id: "us_uuid1" }) + core.utils.getAppIdFromCtx = jest.fn() + core.utils.setCookie = jest.fn() + core.utils.getCookie = jest.fn() + return core }) } @@ -66,30 +51,13 @@ function mockAuthWithCookie() { mockWorker() jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - db: { - ...core.db, - dbExists: () => true, - }, - utils: { - getAppIdFromCtx: () => { - return "app_test" - }, - setCookie: jest.fn(), - clearCookie: jest.fn(), - getCookie: () => ({ appId: "app_different", roleId: "PUBLIC" }), - }, - cache: { - user: { - getUser: async id => { - return { - _id: "us_uuid1", - } - }, - }, - }, - } + core.db.dbExists = () => true + core.cache.user.getUser = () => ({ _id: "us_uuid1" }) + core.utils.getAppIdFromCtx = () => "app_test" + core.utils.getCookie = () => ({ appId: "app_different", roleId: "PUBLIC" }) + core.utils.setCookie = jest.fn() + core.utils.clearCookie = jest.fn() + return core }) } @@ -109,7 +77,7 @@ class TestConfiguration { path: "", cookies: { set: jest.fn(), - } + }, } } @@ -177,29 +145,16 @@ describe("Current app middleware", () => { mockReset() jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - db: { - ...core.db, - dbExists: () => true, - }, - utils: { - getAppIdFromCtx: () => { - return "app_test" - }, - setCookie: jest.fn(), - getCookie: jest.fn(), - }, - cache: { - user: { - getUser: async id => { - return { - _id: "us_uuid1", - } - }, - }, - }, - } + core.db.dbExists = () => true + core.cache.user.getUser = () => ({ _id: "us_uuid1" }) + core.utils.getAppIdFromCtx = () => "app_test" + core.utils.getCookie = () => ({ + appId: "app_different", + roleId: "PUBLIC", + }) + core.utils.setCookie = jest.fn() + core.utils.clearCookie = jest.fn() + return core }) await checkExpected() }) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index affdaa9938..82a1578c88 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,4 +1,3 @@ -import { redis } from "@budibase/backend-core" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { @@ -308,7 +307,7 @@ export const checkInvite = async (ctx: any) => { const { code } = ctx.params let invite try { - invite = await redis.invite.getCode(code) + invite = await cache.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 +321,7 @@ export const checkInvite = async (ctx: any) => { export const getUserInvites = async (ctx: any) => { try { // Restricted to the currently authenticated tenant - ctx.body = await redis.invite.getInviteCodes() + ctx.body = await cache.invite.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } @@ -336,7 +335,7 @@ export const updateInvite = async (ctx: any) => { let invite try { - invite = await redis.invite.getCode(code) + invite = await cache.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") return @@ -364,7 +363,7 @@ export const updateInvite = async (ctx: any) => { } } - await redis.invite.updateCode(code, updated) + await cache.invite.updateCode(code, updated) ctx.body = { ...invite } } @@ -374,8 +373,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.invite.getCode(inviteCode) - await redis.invite.deleteCode(inviteCode) + const { email, info }: any = await cache.invite.getCode(inviteCode) + await cache.invite.deleteCode(inviteCode) const user = await tenancy.doInTenant(info.tenantId, async () => { let request: any = { firstName, diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index e486a67433..34dc90ccd6 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -72,9 +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() + events.shutdown() + await redis.clients.shutdown() await queue.shutdown() if (!env.isTest()) { process.exit(errCode) @@ -89,8 +88,8 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) await initPro() - await redis.invite.init() - await redis.passwordReset.init() + await redis.clients.getInviteClient() + await redis.clients.getPasswordResetClient() // 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 704de9e4b2..1f9da8a260 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -6,7 +6,7 @@ import { sessions, tenancy, utils as coreUtils, - redis, + cache, } from "@budibase/backend-core" import { PlatformLogoutOpts, User } from "@budibase/types" import jwt from "jsonwebtoken" @@ -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.getCode(resetCode) + const { userId } = await cache.passwordReset.getCode(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 530b6ce87f..e57a17a0f4 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -3,9 +3,8 @@ import { EmailTemplatePurpose, TemplateType } from "../constants" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getSettingsTemplateContext } from "./templates" import { processString } from "@budibase/string-templates" -import { redis } from "@budibase/backend-core" import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" -import { configs } from "@budibase/backend-core" +import { configs, cache } from "@budibase/backend-core" import ical from "ical-generator" const nodemailer = require("nodemailer") @@ -61,9 +60,9 @@ async function getLinkCode( ) { switch (purpose) { case EmailTemplatePurpose.PASSWORD_RECOVERY: - return redis.passwordReset.createCode(user._id!, info) + return cache.passwordReset.createCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return redis.invite.createCode(email, info) + return cache.invite.createCode(email, info) default: return null }