Move Invite and PasswordReset code into backend-core.
This commit is contained in:
parent
3d73891f5e
commit
b29cfc600c
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()))]
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" })
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue