diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index e569f27b88..2cfd517941 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -51,6 +51,7 @@ export * from "./constants" // expose package init function import * as db from "./db" + export const init = (opts: any = {}) => { db.init(opts.db) } 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..006a06fe26 --- /dev/null +++ b/packages/backend-core/src/redis/invite.ts @@ -0,0 +1,91 @@ +import { utils, tenancy, redis } 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 + +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 updateCode(code: string, value: Invite) { + 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 createCode(email: string, info: any): Promise { + 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 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." + } + return value +} + +export async function deleteCode(code: string) { + await client.delete(code) +} + +/** + Get all currently available user invitations for the current tenant. +**/ +export async function getInviteCodes(): Promise { + 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..13c1b1d2e6 --- /dev/null +++ b/packages/backend-core/src/redis/passwordReset.ts @@ -0,0 +1,47 @@ +import { redis, utils } from "../" + +const TTL_SECONDS = 60 * 60 + +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). + * @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 createCode(userId: string, info: any): Promise { + 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 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." + } + return value +} 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/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/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/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 3a5bd16bdf..0de42622e6 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 { @@ -49,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 de1a605890..affdaa9938 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,8 +1,4 @@ -import { - checkInviteCode, - getInviteCodes, - updateInviteCode, -} from "../../../utilities/redis" +import { redis } from "@budibase/backend-core" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { @@ -16,6 +12,7 @@ import { Ctx, InviteUserRequest, InviteUsersRequest, + InviteUsersResponse, MigrationType, SaveUserResponse, SearchUsersRequest, @@ -249,59 +246,35 @@ 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") +export const onboardUsers = async ( + ctx: Ctx +) => { + 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) + for (const user of resp.successful) { + user.password = createdPasswords[user.email] + } + ctx.body = { ...resp, created: true } } export const invite = async (ctx: Ctx) => { @@ -328,18 +301,18 @@ 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) => { const { code } = ctx.params let invite try { - invite = await checkInviteCode(code, false) + 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") + return } ctx.body = { email: invite.email, @@ -347,14 +320,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 redis.invite.getInviteCodes() } catch (e) { ctx.throw(400, "There was a problem fetching invites") } - ctx.body = invites } export const updateInvite = async (ctx: any) => { @@ -365,12 +336,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 redis.invite.getCode(code) } catch (e) { ctx.throw(400, "There was a problem with the invite") + return } let updated = { @@ -395,7 +364,7 @@ export const updateInvite = async (ctx: any) => { } } - await updateInviteCode(code, updated) + await redis.invite.updateCode(code, updated) ctx.body = { ...invite } } @@ -405,7 +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 checkInviteCode(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/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 846b98a7ae..a85933255a 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,24 @@ 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) + + jest.clearAllMocks() + + 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( @@ -101,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", () => { @@ -633,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/index.ts b/packages/worker/src/index.ts index 4b1d11ecf7..e486a67433 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -16,13 +16,13 @@ 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" import api from "./api" -import * as redis from "./utilities/redis" const koaSession = require("koa-session") import { userAgent } from "koa-useragent" @@ -72,7 +72,8 @@ server.on("close", async () => { shuttingDown = true console.log("Server Closed") timers.cleanup() - await redis.shutdown() + await redis.invite.shutdown() + await redis.passwordReset.shutdown() await events.shutdown() await queue.shutdown() if (!env.isTest()) { @@ -88,7 +89,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.init() + 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 d989113c3d..704de9e4b2 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.checkResetPasswordCode(resetCode) + const { userId } = await redis.passwordReset.getCode(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..9dc1bac0e9 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" @@ -14,11 +18,13 @@ export async function invite( const matchedEmails = await usersCore.searchExistingEmails( users.map(u => u.email) ) - const newUsers = [] + const newUsers: InviteUserRequest[] = [] // separate duplicates from new users for (let user of users) { 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" }) } else { newUsers.push(user) 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 + } } diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index c5b1d9d8ab..530b6ce87f 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 { redis } from "@budibase/backend-core" 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 redis.passwordReset.createCode(user._id!, info) case EmailTemplatePurpose.INVITATION: - return getInviteCode(email, info) + return redis.invite.createCode(email, info) default: return null } diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts deleted file mode 100644 index 993cdf97ce..0000000000 --- a/packages/worker/src/utilities/redis.ts +++ /dev/null @@ -1,150 +0,0 @@ -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 - } -} - -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 - } -} - -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 -} - -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 -} - -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() -} - -/** - * make sure redis connection is closed. - */ -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") -} - -/** - * 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 getResetPasswordCode(userId: string, info: any) { - return writeACode(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. - * @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 -) { - try { - return getACode(redis.utils.Databases.PW_RESETS, resetCode, deleteCode) - } 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 getInviteCode(email: string, info: any) { - return writeACode(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 -) { - try { - return getACode(redis.utils.Databases.INVITATIONS, inviteCode, deleteCode) - } catch (err) { - throw "Invitation is not valid or has expired, please request a new one." - } -} - -/** - 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() - - const results = 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) -} 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"