Fully type the worker redis utils file.
This commit is contained in:
parent
cfecff479d
commit
3f69b17c94
|
@ -12,7 +12,7 @@ import { generator } from "./generator"
|
||||||
import { tenant } from "."
|
import { tenant } from "."
|
||||||
|
|
||||||
export const newEmail = () => {
|
export const newEmail = () => {
|
||||||
return `${uuid()}@test.com`
|
return `${uuid()}@example.com`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
checkInviteCode,
|
getInviteCode,
|
||||||
|
deleteInviteCode,
|
||||||
getInviteCodes,
|
getInviteCodes,
|
||||||
updateInviteCode,
|
updateInviteCode,
|
||||||
} from "../../../utilities/redis"
|
} from "../../../utilities/redis"
|
||||||
|
@ -336,10 +337,11 @@ export const checkInvite = async (ctx: any) => {
|
||||||
const { code } = ctx.params
|
const { code } = ctx.params
|
||||||
let invite
|
let invite
|
||||||
try {
|
try {
|
||||||
invite = await checkInviteCode(code, false)
|
invite = await getInviteCode(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")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
|
@ -365,12 +367,10 @@ export const updateInvite = async (ctx: any) => {
|
||||||
|
|
||||||
let invite
|
let invite
|
||||||
try {
|
try {
|
||||||
invite = await checkInviteCode(code, false)
|
invite = await getInviteCode(code)
|
||||||
if (!invite) {
|
|
||||||
throw new Error("The invite could not be retrieved")
|
|
||||||
}
|
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated = {
|
let updated = {
|
||||||
|
@ -405,7 +405,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 checkInviteCode(inviteCode)
|
const { email, info }: any = await getInviteCode(inviteCode)
|
||||||
|
await deleteInviteCode(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,
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { InviteUsersResponse, User } from "@budibase/types"
|
import { InviteUsersResponse, User } from "@budibase/types"
|
||||||
|
|
||||||
jest.mock("nodemailer")
|
|
||||||
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
||||||
const sendMailMock = mocks.email.mock()
|
|
||||||
import { events, tenancy, accounts as _accounts } from "@budibase/backend-core"
|
import { events, tenancy, accounts as _accounts } from "@budibase/backend-core"
|
||||||
import * as userSdk from "../../../../sdk/users"
|
import * as userSdk from "../../../../sdk/users"
|
||||||
|
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const sendMailMock = mocks.email.mock()
|
||||||
|
|
||||||
const accounts = jest.mocked(_accounts)
|
const accounts = jest.mocked(_accounts)
|
||||||
|
|
||||||
describe("/api/global/users", () => {
|
describe("/api/global/users", () => {
|
||||||
|
@ -54,6 +55,22 @@ describe("/api/global/users", () => {
|
||||||
expect(events.user.invited).toBeCalledTimes(0)
|
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 () => {
|
it("should be able to create new user from invite", async () => {
|
||||||
const email = structures.users.newEmail()
|
const email = structures.users.newEmail()
|
||||||
const { code } = await config.api.users.sendUserInvite(
|
const { code } = await config.api.users.sendUserInvite(
|
||||||
|
|
|
@ -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.checkResetPasswordCode(resetCode)
|
const { userId } = await redis.getResetPasswordCode(resetCode)
|
||||||
|
|
||||||
let user = await userSdk.db.getUser(userId)
|
let user = await userSdk.db.getUser(userId)
|
||||||
user.password = password
|
user.password = password
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { events, tenancy, users as usersCore } from "@budibase/backend-core"
|
||||||
import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
|
import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
|
||||||
import { sendEmail } from "../../utilities/email"
|
import { sendEmail } from "../../utilities/email"
|
||||||
import { EmailTemplatePurpose } from "../../constants"
|
import { EmailTemplatePurpose } from "../../constants"
|
||||||
|
import { getInviteCodes } from "../..//utilities/redis"
|
||||||
|
|
||||||
export async function invite(
|
export async function invite(
|
||||||
users: InviteUsersRequest
|
users: InviteUsersRequest
|
||||||
|
@ -14,6 +15,7 @@ export async function invite(
|
||||||
const matchedEmails = await usersCore.searchExistingEmails(
|
const matchedEmails = await usersCore.searchExistingEmails(
|
||||||
users.map(u => u.email)
|
users.map(u => u.email)
|
||||||
)
|
)
|
||||||
|
const existingInvites = await getInviteCodes()
|
||||||
const newUsers = []
|
const newUsers = []
|
||||||
|
|
||||||
// separate duplicates from new users
|
// separate duplicates from new users
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 { getResetPasswordCode, getInviteCode } from "./redis"
|
import { createResetPasswordCode, createInviteCode } from "./redis"
|
||||||
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
|
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
|
||||||
import { configs } from "@budibase/backend-core"
|
import { configs } from "@budibase/backend-core"
|
||||||
import ical from "ical-generator"
|
import ical from "ical-generator"
|
||||||
|
@ -61,9 +61,9 @@ async function getLinkCode(
|
||||||
) {
|
) {
|
||||||
switch (purpose) {
|
switch (purpose) {
|
||||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||||
return getResetPasswordCode(user._id!, info)
|
return createResetPasswordCode(user._id!, info)
|
||||||
case EmailTemplatePurpose.INVITATION:
|
case EmailTemplatePurpose.INVITATION:
|
||||||
return getInviteCode(email, info)
|
return createInviteCode(email, info)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +1,24 @@
|
||||||
import { redis, utils, tenancy } from "@budibase/backend-core"
|
import { redis, utils, tenancy } from "@budibase/backend-core"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
function getExpirySecondsForDB(db: string) {
|
interface Invite {
|
||||||
switch (db) {
|
email: string
|
||||||
case redis.utils.Databases.PW_RESETS:
|
info: any
|
||||||
// a hour
|
|
||||||
return 3600
|
|
||||||
case redis.utils.Databases.INVITATIONS:
|
|
||||||
// a week
|
|
||||||
return 604800
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pwResetClient: any, invitationClient: any
|
interface InviteWithCode extends Invite {
|
||||||
|
code: string
|
||||||
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) {
|
interface PasswordReset {
|
||||||
const client = await getClient(db)
|
userId: string
|
||||||
const code = utils.newid()
|
info: any
|
||||||
await client.store(code, value, getExpirySecondsForDB(db))
|
|
||||||
return code
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateACode(db: string, code: string, value: any) {
|
type RedisDBName =
|
||||||
const client = await getClient(db)
|
| redis.utils.Databases.PW_RESETS
|
||||||
await client.store(code, value, getExpirySecondsForDB(db))
|
| redis.utils.Databases.INVITATIONS
|
||||||
}
|
let pwResetClient: redis.Client, invitationClient: redis.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(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() {
|
export async function init() {
|
||||||
pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS)
|
pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS)
|
||||||
|
@ -63,9 +27,6 @@ export async function init() {
|
||||||
await invitationClient.init()
|
await invitationClient.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* make sure redis connection is closed.
|
|
||||||
*/
|
|
||||||
export async function shutdown() {
|
export async function shutdown() {
|
||||||
if (pwResetClient) await pwResetClient.finish()
|
if (pwResetClient) await pwResetClient.finish()
|
||||||
if (invitationClient) await invitationClient.finish()
|
if (invitationClient) await invitationClient.finish()
|
||||||
|
@ -74,6 +35,65 @@ export async function shutdown() {
|
||||||
console.log("Redis 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.
|
* 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).
|
||||||
|
@ -81,22 +101,20 @@ export async function shutdown() {
|
||||||
* @param info Info about the user/the reset process.
|
* @param info Info about the user/the reset process.
|
||||||
* @return returns the code that was stored to redis.
|
* @return returns the code that was stored to redis.
|
||||||
*/
|
*/
|
||||||
export async function getResetPasswordCode(userId: string, info: any) {
|
export async function createResetPasswordCode(userId: string, info: any) {
|
||||||
return writeACode(redis.utils.Databases.PW_RESETS, { userId, info })
|
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 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
|
* @return returns the user ID if it is found
|
||||||
*/
|
*/
|
||||||
export async function checkResetPasswordCode(
|
export async function getResetPasswordCode(
|
||||||
resetCode: string,
|
code: string
|
||||||
deleteCode = true
|
): Promise<PasswordReset> {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
return getACode(redis.utils.Databases.PW_RESETS, resetCode, deleteCode)
|
return getCode(redis.utils.Databases.PW_RESETS, code)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw "Provided information is not valid, cannot reset password - please try again."
|
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.
|
* @param info Information to be carried along with the invitation.
|
||||||
* @return returns the code that was stored to redis.
|
* @return returns the code that was stored to redis.
|
||||||
*/
|
*/
|
||||||
export async function getInviteCode(email: string, info: any) {
|
export async function createInviteCode(email: string, info: any) {
|
||||||
return writeACode(redis.utils.Databases.INVITATIONS, { email, info })
|
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.
|
* 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 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.
|
* @return If the code is valid then an email address will be returned.
|
||||||
*/
|
*/
|
||||||
export async function checkInviteCode(
|
export async function getInviteCode(code: string): Promise<Invite> {
|
||||||
inviteCode: string,
|
|
||||||
deleteCode: boolean = true
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
return getACode(redis.utils.Databases.INVITATIONS, inviteCode, deleteCode)
|
return getCode(redis.utils.Databases.INVITATIONS, code)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteInviteCode(code: string) {
|
||||||
|
return deleteCode(redis.utils.Databases.INVITATIONS, 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() {
|
export async function getInviteCodes(): Promise<InviteWithCode[]> {
|
||||||
const client = await getClient(redis.utils.Databases.INVITATIONS)
|
const client = getClient(redis.utils.Databases.INVITATIONS)
|
||||||
const invites: any[] = await client.scan()
|
const invites: { key: string; value: Invite }[] = await client.scan()
|
||||||
|
|
||||||
const results = invites.map(invite => {
|
const results: InviteWithCode[] = invites.map(invite => {
|
||||||
return {
|
return {
|
||||||
...invite.value,
|
...invite.value,
|
||||||
code: invite.key,
|
code: invite.key,
|
||||||
|
|
Loading…
Reference in New Issue