Fleshed out fully all redis interactions for invitations and password resets.
This commit is contained in:
parent
1cf778845a
commit
b372d2a824
|
@ -29,7 +29,7 @@ module.exports = {
|
|||
},
|
||||
db: require("./db/utils"),
|
||||
redis: {
|
||||
client: require("./redis"),
|
||||
Client: require("./redis"),
|
||||
utils: require("./redis/utils"),
|
||||
},
|
||||
utils: {
|
||||
|
|
|
@ -12,6 +12,11 @@ let CLIENT
|
|||
*/
|
||||
function init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// if a connection existed, close it and re-create it
|
||||
if (CLIENT) {
|
||||
CLIENT.disconnect()
|
||||
CLIENT = null
|
||||
}
|
||||
const { opts, host, port } = getRedisOptions(CLUSTERED)
|
||||
if (CLUSTERED) {
|
||||
CLIENT = new Redis.Cluster([{ host, port }], opts)
|
||||
|
@ -78,6 +83,10 @@ class RedisWrapper {
|
|||
return this
|
||||
}
|
||||
|
||||
async finish() {
|
||||
this._client.disconnect()
|
||||
}
|
||||
|
||||
async scan() {
|
||||
const db = this._db,
|
||||
client = this._client
|
||||
|
|
|
@ -6,6 +6,7 @@ const SEPARATOR = "-"
|
|||
|
||||
exports.Databases = {
|
||||
PW_RESETS: "pwReset",
|
||||
INVITATIONS: "invitation",
|
||||
}
|
||||
|
||||
exports.getRedisOptions = (clustered = false) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ const authPkg = require("@budibase/auth")
|
|||
const { google } = require("@budibase/auth/src/middleware")
|
||||
const { Configs } = require("../../../constants")
|
||||
const CouchDB = require("../../../db")
|
||||
const { sendEmail } = require("../../../utilities/email")
|
||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
||||
const { Cookies } = authPkg.constants
|
||||
const { passport } = authPkg.auth
|
||||
|
@ -44,14 +44,19 @@ exports.authenticate = async (ctx, next) => {
|
|||
*/
|
||||
exports.reset = async ctx => {
|
||||
const { email } = ctx.request.body
|
||||
try {
|
||||
const user = getGlobalUserByEmail(email)
|
||||
if (user) {
|
||||
const configured = await isEmailConfigured()
|
||||
if (!configured) {
|
||||
throw "Please contact your platform administrator, SMTP is not configured."
|
||||
}
|
||||
try {
|
||||
const user = await getGlobalUserByEmail(email)
|
||||
sendEmail()
|
||||
} catch (err) {
|
||||
// don't throw any kind of error to the user, this might give away something
|
||||
}
|
||||
ctx.body = {}
|
||||
ctx.body = {
|
||||
message: "If user exists an email has been sent."
|
||||
}
|
||||
}
|
||||
|
||||
exports.logout = async ctx => {
|
||||
|
|
|
@ -45,6 +45,8 @@ const TemplateBindings = {
|
|||
LOGIN_URL: "loginUrl",
|
||||
CURRENT_YEAR: "currentYear",
|
||||
CURRENT_DATE: "currentDate",
|
||||
RESET_CODE: "resetCode",
|
||||
INVITE_CODE: "inviteCode",
|
||||
}
|
||||
|
||||
const TemplateMetadata = {
|
||||
|
|
|
@ -5,6 +5,7 @@ const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants")
|
|||
const { getTemplateByPurpose } = require("../constants/templates")
|
||||
const { getSettingsTemplateContext } = require("./templates")
|
||||
const { processString } = require("@budibase/string-templates")
|
||||
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
||||
|
||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||
const TYPE = TemplateTypes.EMAIL
|
||||
|
@ -30,6 +31,24 @@ function createSMTPTransport(config) {
|
|||
return nodemailer.createTransport(options)
|
||||
}
|
||||
|
||||
async function getLinkCode(purpose, email, user) {
|
||||
switch (purpose) {
|
||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||
return getResetPasswordCode(user._id)
|
||||
case EmailTemplatePurpose.INVITATION:
|
||||
return getInviteCode(email)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
||||
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
||||
* @param {string} email the address which it is being sent to for contextual purposes.
|
||||
* @param {object|null} user If being sent to an existing user then the object can be provided for context.
|
||||
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
||||
*/
|
||||
async function buildEmail(purpose, email, user) {
|
||||
// this isn't a full email
|
||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||
|
@ -47,9 +66,10 @@ async function buildEmail(purpose, email, user) {
|
|||
styles = styles.contents
|
||||
body = body.contents
|
||||
|
||||
// TODO: need to extend the context as much as possible
|
||||
// if there is a link code needed this will retrieve it
|
||||
const code = await getLinkCode(purpose, email, user)
|
||||
const context = {
|
||||
...(await getSettingsTemplateContext()),
|
||||
...(await getSettingsTemplateContext(purpose, code)),
|
||||
email,
|
||||
user: user || {},
|
||||
}
|
||||
|
@ -64,19 +84,46 @@ async function buildEmail(purpose, email, user) {
|
|||
})
|
||||
}
|
||||
|
||||
exports.sendEmail = async (email, purpose, { groupId, userId }) => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
/**
|
||||
* Utility function for finding most valid SMTP configuration.
|
||||
* @param {object} db The CouchDB database which is to be looked up within.
|
||||
* @param {string|null} groupId If using finer grain control of configs a group can be used.
|
||||
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
||||
*/
|
||||
async function getSmtpConfiguration(db, groupId = null) {
|
||||
const params = {
|
||||
type: Configs.SMTP,
|
||||
}
|
||||
if (groupId) {
|
||||
params.group = groupId
|
||||
}
|
||||
let user = {}
|
||||
if (userId) {
|
||||
user = db.get(userId)
|
||||
return determineScopedConfig(db, params)
|
||||
}
|
||||
const config = await determineScopedConfig(db, params)
|
||||
|
||||
/**
|
||||
* Checks if a SMTP config exists based on passed in parameters.
|
||||
* @param groupId
|
||||
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
||||
*/
|
||||
exports.isEmailConfigured = async (groupId = null) => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const config = await getSmtpConfiguration(db, groupId)
|
||||
return config != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an email address and an email purpose this will retrieve the SMTP configuration and
|
||||
* send an email using it.
|
||||
* @param {string} email The email address to send to.
|
||||
* @param {string} purpose The purpose of the email being sent (e.g. reset password).
|
||||
* @param {string|null} groupId If finer grain controls being used then this will lookup config for group.
|
||||
* @param {object|null} if sending to an existing user the object can be provided, this is used in the context.
|
||||
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
||||
* nodemailer response.
|
||||
*/
|
||||
exports.sendEmail = async (email, purpose, { groupId, user }) => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const config = await getSmtpConfiguration(db, groupId)
|
||||
if (!config) {
|
||||
throw "Unable to find SMTP configuration"
|
||||
}
|
||||
|
@ -90,6 +137,11 @@ exports.sendEmail = async (email, purpose, { groupId, userId }) => {
|
|||
return transport.sendMail(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an SMTP configuration this runs it through nodemailer to see if it is infact functional.
|
||||
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
|
||||
* @return {Promise<boolean>} returns true if the configuration is valid.
|
||||
*/
|
||||
exports.verifyConfig = async config => {
|
||||
const transport = createSMTPTransport(config)
|
||||
await transport.verify()
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
const { Client, utils } = require("@budibase/auth").redis
|
||||
const { newid } = require("@budibase/auth").utils
|
||||
|
||||
const EXPIRE_TOKEN_SECONDS = 3600
|
||||
|
||||
async function getClient(db) {
|
||||
return await (new Client(db)).init()
|
||||
}
|
||||
|
||||
async function writeACode(db, value) {
|
||||
const client = await getClient(db)
|
||||
const code = newid()
|
||||
await client.store(code, value, EXPIRE_TOKEN_SECONDS)
|
||||
client.finish()
|
||||
return code
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {string} userId the ID of the user which is to be reset.
|
||||
* @return {Promise<string>} returns the code that was stored to redis.
|
||||
*/
|
||||
exports.getResetPasswordCode = async userId => {
|
||||
return writeACode(utils.Databases.PW_RESETS, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a reset code this will lookup to redis, check if the code is valid and delete if required.
|
||||
* @param {string} resetCode The code provided via the email link.
|
||||
* @param {boolean} deleteCode If the code is used/finished with this will delete it.
|
||||
* @return {Promise<string>} returns the user ID if it is found
|
||||
*/
|
||||
exports.checkResetPasswordCode = async (resetCode, deleteCode = false) => {
|
||||
const client = await getClient(utils.Databases.PW_RESETS)
|
||||
const userId = await client.get(resetCode)
|
||||
if (deleteCode) {
|
||||
await client.delete(resetCode)
|
||||
}
|
||||
client.finish()
|
||||
return userId
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an invitation code and writes it to redis - which can later be checked for user creation.
|
||||
* @param {string} email the email address which the code is being sent to (for use later).
|
||||
* @return {Promise<string>} returns the code that was stored to redis.
|
||||
*/
|
||||
exports.getInviteCode = async email => {
|
||||
return writeACode(utils.Databases.INVITATIONS, email)
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
const CouchDB = require("../db")
|
||||
const { getConfigParams, StaticDatabases } = require("@budibase/auth").db
|
||||
const { Configs, TemplateBindings, LOGO_URL } = require("../constants")
|
||||
const { Configs, TemplateBindings, LOGO_URL, EmailTemplatePurpose } = require("../constants")
|
||||
const { checkSlashesInUrl } = require("./index")
|
||||
const env = require("../environment")
|
||||
|
||||
const LOCAL_URL = `http://localhost:${env.PORT}`
|
||||
const BASE_COMPANY = "Budibase"
|
||||
|
||||
exports.getSettingsTemplateContext = async () => {
|
||||
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||
const response = await db.allDocs(
|
||||
getConfigParams(Configs.SETTINGS, {
|
||||
|
@ -18,15 +18,10 @@ exports.getSettingsTemplateContext = async () => {
|
|||
if (!settings.platformUrl) {
|
||||
settings.platformUrl = LOCAL_URL
|
||||
}
|
||||
// TODO: need to fully spec out the context
|
||||
const URL = settings.platformUrl
|
||||
return {
|
||||
const context = {
|
||||
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
||||
[TemplateBindings.PLATFORM_URL]: URL,
|
||||
[TemplateBindings.REGISTRATION_URL]: checkSlashesInUrl(
|
||||
`${URL}/registration`
|
||||
),
|
||||
[TemplateBindings.RESET_URL]: checkSlashesInUrl(`${URL}/reset`),
|
||||
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
||||
[TemplateBindings.DOCS_URL]:
|
||||
settings.docsUrl || "https://docs.budibase.com/",
|
||||
|
@ -34,4 +29,20 @@ exports.getSettingsTemplateContext = async () => {
|
|||
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
||||
[TemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
|
||||
}
|
||||
// attach purpose specific context
|
||||
switch (purpose) {
|
||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||
context[TemplateBindings.RESET_CODE] = code
|
||||
context[TemplateBindings.RESET_URL] = checkSlashesInUrl(
|
||||
`${URL}/reset/${code}`
|
||||
)
|
||||
break
|
||||
case EmailTemplatePurpose.INVITATION:
|
||||
context[TemplateBindings.INVITE_CODE] = code
|
||||
context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl(
|
||||
`${URL}/registration/${code}`
|
||||
)
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue