Merge branch 'master' into fix/build-references
This commit is contained in:
commit
00f65f396e
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,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<string> {
|
||||
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<Invite> {
|
||||
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<InviteWithCode[]> {
|
||||
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,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<string> {
|
||||
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<PasswordReset> {
|
||||
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
|
||||
}
|
|
@ -303,7 +303,7 @@ export class UserDB {
|
|||
|
||||
static async bulkCreate(
|
||||
newUsersRequested: User[],
|
||||
groups: string[]
|
||||
groups?: string[]
|
||||
): Promise<BulkUserCreated> {
|
||||
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)
|
||||
|
|
|
@ -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()))]
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Omit<User, "userId">>): User => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<InviteUsersRequest>) => {
|
||||
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<InviteUsersRequest, InviteUsersResponse>
|
||||
) => {
|
||||
if (await isEmailConfigured()) {
|
||||
await inviteMultiple(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
let createdPasswords: Record<string, string> = {}
|
||||
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<InviteUserRequest>) => {
|
||||
|
@ -328,18 +301,18 @@ export const invite = async (ctx: Ctx<InviteUserRequest>) => {
|
|||
}
|
||||
|
||||
export const inviteMultiple = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<InviteUsersResponse> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue