Move Invite and PasswordReset code into backend-core.

This commit is contained in:
Sam Rose 2023-11-09 14:51:07 +00:00
parent 3d73891f5e
commit b29cfc600c
No known key found for this signature in database
10 changed files with 159 additions and 185 deletions

View File

@ -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"

View File

@ -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<redis.Client> {
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<string> {
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<Invite> {
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<InviteWithCode[]> {
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<InviteWithCode[]> {
return (await getInviteCodes()).filter(invite =>
emails.includes(invite.email)
)
}

View File

@ -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<redis.Client> {
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<string> {
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<PasswordReset> {
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
}

View File

@ -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()))]
}

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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" })

View File

@ -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"

View File

@ -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<PasswordReset> {
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<Invite> {
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<InviteWithCode[]> {
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)
}