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.

This commit is contained in:
mike12345567 2023-11-17 16:20:10 +00:00
parent 7412fd5a5c
commit 7fb6c0927a
12 changed files with 95 additions and 141 deletions

View File

@ -1,6 +1,6 @@
const BaseCache = require("./base") import BaseCache from "./base"
const GENERIC = new BaseCache.default() const GENERIC = new BaseCache()
export enum CacheKey { export enum CacheKey {
CHECKLIST = "checklist", CHECKLIST = "checklist",
@ -18,13 +18,9 @@ export enum TTL {
ONE_DAY = 86400, ONE_DAY = 86400,
} }
function performExport(funcName: string) { export const keys = GENERIC.keys
return (...args: any) => GENERIC[funcName](...args) export const get = GENERIC.get
} export const store = GENERIC.store
export const destroy = GENERIC.delete
export const keys = performExport("keys") export const withCache = GENERIC.withCache
export const get = performExport("get") export const bustCache = GENERIC.bustCache
export const store = performExport("store")
export const destroy = performExport("delete")
export const withCache = performExport("withCache")
export const bustCache = performExport("bustCache")

View File

@ -2,4 +2,6 @@ export * as generic from "./generic"
export * as user from "./user" export * as user from "./user"
export * as app from "./appMetadata" export * as app from "./appMetadata"
export * as writethrough from "./writethrough" export * as writethrough from "./writethrough"
export * as invite from "./invite"
export * as passwordReset from "./passwordReset"
export * from "./generic" export * from "./generic"

View File

@ -1,7 +1,10 @@
import { utils, tenancy, redis } from "../" import * as utils from "../utils"
import { Duration, DurationType } from "../utils"
import env from "../environment" 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 { interface Invite {
email: string email: string
@ -12,25 +15,13 @@ interface InviteWithCode extends Invite {
code: string 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 * 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 * @param value The body of the updated user invitation
*/ */
export async function updateCode(code: string, value: Invite) { export async function updateCode(code: string, value: Invite) {
const client = await redis.getInviteClient()
await client.store(code, value, TTL_SECONDS) 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<string> { export async function createCode(email: string, info: any): Promise<string> {
const code = utils.newid() const code = utils.newid()
const client = await redis.getInviteClient()
await client.store(code, { email, info }, TTL_SECONDS) await client.store(code, { email, info }, TTL_SECONDS)
return code return code
} }
/** /**
* Checks that the provided invite code is valid - will return the email address of user that was invited. * 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. * @return If the code is valid then an email address will be returned.
*/ */
export async function getCode(code: string): Promise<Invite> { export async function getCode(code: string): Promise<Invite> {
const client = await redis.getInviteClient()
const value = (await client.get(code)) as Invite | undefined const value = (await client.get(code)) as Invite | undefined
if (!value) { if (!value) {
throw "Invitation is not valid or has expired, please request a new one." throw "Invitation is not valid or has expired, please request a new one."
@ -60,13 +53,15 @@ export async function getCode(code: string): Promise<Invite> {
} }
export async function deleteCode(code: string) { export async function deleteCode(code: string) {
const client = await redis.getInviteClient()
await client.delete(code) 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<InviteWithCode[]> { export async function getInviteCodes(): Promise<InviteWithCode[]> {
const client = await redis.getInviteClient()
const invites: { key: string; value: Invite }[] = await client.scan() const invites: { key: string; value: Invite }[] = await client.scan()
const results: InviteWithCode[] = invites.map(invite => { const results: InviteWithCode[] = invites.map(invite => {
@ -78,7 +73,7 @@ export async function getInviteCodes(): Promise<InviteWithCode[]> {
if (!env.MULTI_TENANCY) { if (!env.MULTI_TENANCY) {
return results return results
} }
const tenantId = tenancy.getTenantId() const tenantId = getTenantId()
return results.filter(invite => tenantId === invite.info.tenantId) return results.filter(invite => tenantId === invite.info.tenantId)
} }

View File

@ -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 { interface PasswordReset {
userId: string userId: string
info: any 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. * 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). * 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<string> { export async function createCode(userId: string, info: any): Promise<string> {
const code = utils.newid() const code = utils.newid()
const client = await redis.getPasswordResetClient()
await client.store(code, { userId, info }, TTL_SECONDS) await client.store(code, { userId, info }, TTL_SECONDS)
return code return code
} }
@ -39,6 +29,7 @@ export async function createCode(userId: string, info: any): Promise<string> {
* @return returns the user ID if it is found * @return returns the user ID if it is found
*/ */
export async function getCode(code: string): Promise<PasswordReset> { export async function getCode(code: string): Promise<PasswordReset> {
const client = await redis.getPasswordResetClient()
const value = (await client.get(code)) as PasswordReset | undefined const value = (await client.get(code)) as PasswordReset | undefined
if (!value) { if (!value) {
throw "Provided information is not valid, cannot reset password - please try again." throw "Provided information is not valid, cannot reset password - please try again."

View File

@ -4,5 +4,3 @@ export { default as Client } from "./redis"
export * as utils from "./utils" export * as utils from "./utils"
export * as clients from "./init" export * as clients from "./init"
export * as locks from "./redlockImpl" export * as locks from "./redlockImpl"
export * as invite from "./invite"
export * as passwordReset from "./passwordReset"

View File

@ -7,7 +7,9 @@ let userClient: Client,
cacheClient: Client, cacheClient: Client,
writethroughClient: Client, writethroughClient: Client,
lockClient: Client, lockClient: Client,
socketClient: Client socketClient: Client,
inviteClient: Client,
passwordResetClient: Client
async function init() { async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).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() cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
lockClient = await new Client(utils.Databases.LOCKS).init() lockClient = await new Client(utils.Databases.LOCKS).init()
writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).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( socketClient = await new Client(
utils.Databases.SOCKET_IO, utils.Databases.SOCKET_IO,
utils.SelectableDatabase.SOCKET_IO utils.SelectableDatabase.SOCKET_IO
@ -29,6 +33,8 @@ export async function shutdown() {
if (cacheClient) await cacheClient.finish() if (cacheClient) await cacheClient.finish()
if (writethroughClient) await writethroughClient.finish() if (writethroughClient) await writethroughClient.finish()
if (lockClient) await lockClient.finish() if (lockClient) await lockClient.finish()
if (inviteClient) await inviteClient.finish()
if (passwordResetClient) await passwordResetClient.finish()
if (socketClient) await socketClient.finish() if (socketClient) await socketClient.finish()
} }
@ -84,3 +90,17 @@ export async function getSocketClient() {
} }
return socketClient return socketClient
} }
export async function getInviteClient() {
if (!inviteClient) {
await init()
}
return inviteClient
}
export async function getPasswordResetClient() {
if (!passwordResetClient) {
await init()
}
return passwordResetClient
}

View File

@ -6,7 +6,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { ViewName } from "../constants" import { ViewName } from "../constants"
import { getExistingInvites } from "../redis/invite" import { getExistingInvites } from "../cache/invite"
/** /**
* Apply a system-wide search on emails: * Apply a system-wide search on emails:

View File

@ -9,11 +9,11 @@ function mockWorker() {
return { return {
_id: "us_uuid1", _id: "us_uuid1",
roles: { roles: {
"app_test": "BASIC", app_test: "BASIC",
}, },
roleId: "BASIC", roleId: "BASIC",
} }
} },
})) }))
} }
@ -37,27 +37,12 @@ function mockAuthWithNoCookie() {
mockWorker() mockWorker()
jest.mock("@budibase/backend-core", () => { jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core") const core = jest.requireActual("@budibase/backend-core")
return { core.db.dbExists = () => true
...core, core.cache.user.getUser = () => ({ _id: "us_uuid1" })
db: { core.utils.getAppIdFromCtx = jest.fn()
...core.db, core.utils.setCookie = jest.fn()
dbExists: () => true, core.utils.getCookie = jest.fn()
}, return core
cache: {
user: {
getUser: async id => {
return {
_id: "us_uuid1",
}
},
},
},
utils: {
getAppIdFromCtx: jest.fn(),
setCookie: jest.fn(),
getCookie: jest.fn(),
},
}
}) })
} }
@ -66,30 +51,13 @@ function mockAuthWithCookie() {
mockWorker() mockWorker()
jest.mock("@budibase/backend-core", () => { jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core") const core = jest.requireActual("@budibase/backend-core")
return { core.db.dbExists = () => true
...core, core.cache.user.getUser = () => ({ _id: "us_uuid1" })
db: { core.utils.getAppIdFromCtx = () => "app_test"
...core.db, core.utils.getCookie = () => ({ appId: "app_different", roleId: "PUBLIC" })
dbExists: () => true, core.utils.setCookie = jest.fn()
}, core.utils.clearCookie = jest.fn()
utils: { return core
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",
}
},
},
},
}
}) })
} }
@ -109,7 +77,7 @@ class TestConfiguration {
path: "", path: "",
cookies: { cookies: {
set: jest.fn(), set: jest.fn(),
} },
} }
} }
@ -177,29 +145,16 @@ describe("Current app middleware", () => {
mockReset() mockReset()
jest.mock("@budibase/backend-core", () => { jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core") const core = jest.requireActual("@budibase/backend-core")
return { core.db.dbExists = () => true
...core, core.cache.user.getUser = () => ({ _id: "us_uuid1" })
db: { core.utils.getAppIdFromCtx = () => "app_test"
...core.db, core.utils.getCookie = () => ({
dbExists: () => true, appId: "app_different",
}, roleId: "PUBLIC",
utils: { })
getAppIdFromCtx: () => { core.utils.setCookie = jest.fn()
return "app_test" core.utils.clearCookie = jest.fn()
}, return core
setCookie: jest.fn(),
getCookie: jest.fn(),
},
cache: {
user: {
getUser: async id => {
return {
_id: "us_uuid1",
}
},
},
},
}
}) })
await checkExpected() await checkExpected()
}) })

View File

@ -1,4 +1,3 @@
import { redis } from "@budibase/backend-core"
import * as userSdk from "../../../sdk/users" import * as userSdk from "../../../sdk/users"
import env from "../../../environment" import env from "../../../environment"
import { import {
@ -308,7 +307,7 @@ export const checkInvite = async (ctx: any) => {
const { code } = ctx.params const { code } = ctx.params
let invite let invite
try { try {
invite = await redis.invite.getCode(code) invite = await cache.invite.getCode(code)
} catch (e) { } catch (e) {
console.warn("Error getting invite from code", e) console.warn("Error getting invite from code", e)
ctx.throw(400, "There was a problem with the invite") 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) => { export const getUserInvites = async (ctx: any) => {
try { try {
// Restricted to the currently authenticated tenant // Restricted to the currently authenticated tenant
ctx.body = await redis.invite.getInviteCodes() ctx.body = await cache.invite.getInviteCodes()
} catch (e) { } catch (e) {
ctx.throw(400, "There was a problem fetching invites") ctx.throw(400, "There was a problem fetching invites")
} }
@ -336,7 +335,7 @@ export const updateInvite = async (ctx: any) => {
let invite let invite
try { try {
invite = await redis.invite.getCode(code) invite = await cache.invite.getCode(code)
} catch (e) { } catch (e) {
ctx.throw(400, "There was a problem with the invite") ctx.throw(400, "There was a problem with the invite")
return 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 } ctx.body = { ...invite }
} }
@ -374,8 +373,8 @@ export const inviteAccept = async (
const { inviteCode, password, firstName, lastName } = ctx.request.body const { inviteCode, password, firstName, lastName } = ctx.request.body
try { try {
// info is an extension of the user object that was stored by global // info is an extension of the user object that was stored by global
const { email, info }: any = await redis.invite.getCode(inviteCode) const { email, info }: any = await cache.invite.getCode(inviteCode)
await redis.invite.deleteCode(inviteCode) await cache.invite.deleteCode(inviteCode)
const user = await tenancy.doInTenant(info.tenantId, async () => { const user = await tenancy.doInTenant(info.tenantId, async () => {
let request: any = { let request: any = {
firstName, firstName,

View File

@ -72,9 +72,8 @@ server.on("close", async () => {
shuttingDown = true shuttingDown = true
console.log("Server Closed") console.log("Server Closed")
timers.cleanup() timers.cleanup()
await redis.invite.shutdown() events.shutdown()
await redis.passwordReset.shutdown() await redis.clients.shutdown()
await events.shutdown()
await queue.shutdown() await queue.shutdown()
if (!env.isTest()) { if (!env.isTest()) {
process.exit(errCode) process.exit(errCode)
@ -89,8 +88,8 @@ const shutdown = () => {
export default server.listen(parseInt(env.PORT || "4002"), async () => { export default server.listen(parseInt(env.PORT || "4002"), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`) console.log(`Worker running on ${JSON.stringify(server.address())}`)
await initPro() await initPro()
await redis.invite.init() await redis.clients.getInviteClient()
await redis.passwordReset.init() await redis.clients.getPasswordResetClient()
// configure events to use the pro audit log write // configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues // can't integrate directly into backend-core due to cyclic issues
await events.processors.init(proSdk.auditLogs.write) await events.processors.init(proSdk.auditLogs.write)

View File

@ -6,7 +6,7 @@ import {
sessions, sessions,
tenancy, tenancy,
utils as coreUtils, utils as coreUtils,
redis, cache,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { PlatformLogoutOpts, User } from "@budibase/types" import { PlatformLogoutOpts, User } from "@budibase/types"
import jwt from "jsonwebtoken" 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. * Perform the user password update if the provided reset code is valid.
*/ */
export const resetUpdate = async (resetCode: string, password: string) => { 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) let user = await userSdk.db.getUser(userId)
user.password = password user.password = password

View File

@ -3,9 +3,8 @@ import { EmailTemplatePurpose, TemplateType } from "../constants"
import { getTemplateByPurpose, EmailTemplates } from "../constants/templates" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates" import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { redis } from "@budibase/backend-core"
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types" 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" import ical from "ical-generator"
const nodemailer = require("nodemailer") const nodemailer = require("nodemailer")
@ -61,9 +60,9 @@ async function getLinkCode(
) { ) {
switch (purpose) { switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY: case EmailTemplatePurpose.PASSWORD_RECOVERY:
return redis.passwordReset.createCode(user._id!, info) return cache.passwordReset.createCode(user._id!, info)
case EmailTemplatePurpose.INVITATION: case EmailTemplatePurpose.INVITATION:
return redis.invite.createCode(email, info) return cache.invite.createCode(email, info)
default: default:
return null return null
} }