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 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"
|
||||||
|
|
|
@ -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"
|
} 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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a system-wide search on emails:
|
* Apply a system-wide search on emails:
|
||||||
|
@ -26,6 +27,9 @@ export async function searchExistingEmails(emails: string[]) {
|
||||||
const existingAccounts = await getExistingAccounts(emails)
|
const existingAccounts = await getExistingAccounts(emails)
|
||||||
matchedEmails.push(...existingAccounts.map(account => account.email))
|
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()))]
|
return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
deleteInviteCode,
|
deleteInviteCode,
|
||||||
getInviteCodes,
|
getInviteCodes,
|
||||||
updateInviteCode,
|
updateInviteCode,
|
||||||
} from "../../../utilities/redis"
|
} from "@budibase/backend-core/src/redis/invite"
|
||||||
import * as userSdk from "../../../sdk/users"
|
import * as userSdk from "../../../sdk/users"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import Koa from "koa"
|
||||||
import koaBody from "koa-body"
|
import koaBody from "koa-body"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import api from "./api"
|
import api from "./api"
|
||||||
import * as redis from "./utilities/redis"
|
|
||||||
|
|
||||||
const koaSession = require("koa-session")
|
const koaSession = require("koa-session")
|
||||||
import { userAgent } from "koa-useragent"
|
import { userAgent } from "koa-useragent"
|
||||||
|
@ -72,7 +71,6 @@ server.on("close", async () => {
|
||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
console.log("Server Closed")
|
console.log("Server Closed")
|
||||||
timers.cleanup()
|
timers.cleanup()
|
||||||
await redis.shutdown()
|
|
||||||
await events.shutdown()
|
await events.shutdown()
|
||||||
await queue.shutdown()
|
await queue.shutdown()
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
|
@ -88,7 +86,6 @@ 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.init()
|
|
||||||
// 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)
|
||||||
|
|
|
@ -6,12 +6,12 @@ import {
|
||||||
sessions,
|
sessions,
|
||||||
tenancy,
|
tenancy,
|
||||||
utils as coreUtils,
|
utils as coreUtils,
|
||||||
|
redis,
|
||||||
} 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"
|
||||||
import * as userSdk from "../users"
|
import * as userSdk from "../users"
|
||||||
import * as emails from "../../utilities/email"
|
import * as emails from "../../utilities/email"
|
||||||
import * as redis from "../../utilities/redis"
|
|
||||||
import { EmailTemplatePurpose } from "../../constants"
|
import { EmailTemplatePurpose } from "../../constants"
|
||||||
|
|
||||||
// LOGIN / LOGOUT
|
// LOGIN / LOGOUT
|
||||||
|
@ -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.getResetPasswordCode(resetCode)
|
const { userId } = await redis.passwordReset.getResetPasswordCode(resetCode)
|
||||||
|
|
||||||
let user = await userSdk.db.getUser(userId)
|
let user = await userSdk.db.getUser(userId)
|
||||||
user.password = password
|
user.password = password
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
} from "@budibase/types"
|
} 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
|
||||||
|
@ -19,15 +18,11 @@ 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 invitedEmails = (await getInviteCodes()).map(invite => invite.email)
|
|
||||||
const newUsers: InviteUserRequest[] = []
|
const newUsers: InviteUserRequest[] = []
|
||||||
|
|
||||||
// separate duplicates from new users
|
// separate duplicates from new users
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
if (
|
if (matchedEmails.includes(user.email)) {
|
||||||
matchedEmails.includes(user.email) ||
|
|
||||||
invitedEmails.includes(user.email)
|
|
||||||
) {
|
|
||||||
// This "Unavailable" is load bearing. The tests and frontend both check for it
|
// This "Unavailable" is load bearing. The tests and frontend both check for it
|
||||||
// specifically
|
// specifically
|
||||||
response.unsuccessful.push({ email: user.email, reason: "Unavailable" })
|
response.unsuccessful.push({ email: user.email, reason: "Unavailable" })
|
||||||
|
|
|
@ -3,7 +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 { 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 { 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"
|
||||||
|
|
|
@ -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