Fleshed out fully all redis interactions for invitations and password resets.
This commit is contained in:
parent
66341473ca
commit
e31eb5d3d2
|
@ -29,7 +29,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
db: require("./db/utils"),
|
db: require("./db/utils"),
|
||||||
redis: {
|
redis: {
|
||||||
client: require("./redis"),
|
Client: require("./redis"),
|
||||||
utils: require("./redis/utils"),
|
utils: require("./redis/utils"),
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
|
|
|
@ -12,6 +12,11 @@ let CLIENT
|
||||||
*/
|
*/
|
||||||
function init() {
|
function init() {
|
||||||
return new Promise((resolve, reject) => {
|
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)
|
const { opts, host, port } = getRedisOptions(CLUSTERED)
|
||||||
if (CLUSTERED) {
|
if (CLUSTERED) {
|
||||||
CLIENT = new Redis.Cluster([{ host, port }], opts)
|
CLIENT = new Redis.Cluster([{ host, port }], opts)
|
||||||
|
@ -78,6 +83,10 @@ class RedisWrapper {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async finish() {
|
||||||
|
this._client.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
async scan() {
|
async scan() {
|
||||||
const db = this._db,
|
const db = this._db,
|
||||||
client = this._client
|
client = this._client
|
||||||
|
|
|
@ -6,6 +6,7 @@ const SEPARATOR = "-"
|
||||||
|
|
||||||
exports.Databases = {
|
exports.Databases = {
|
||||||
PW_RESETS: "pwReset",
|
PW_RESETS: "pwReset",
|
||||||
|
INVITATIONS: "invitation",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getRedisOptions = (clustered = false) => {
|
exports.getRedisOptions = (clustered = false) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ const authPkg = require("@budibase/auth")
|
||||||
const { google } = require("@budibase/auth/src/middleware")
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
const { Configs } = require("../../../constants")
|
const { Configs } = require("../../../constants")
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const { sendEmail } = require("../../../utilities/email")
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
||||||
const { Cookies } = authPkg.constants
|
const { Cookies } = authPkg.constants
|
||||||
const { passport } = authPkg.auth
|
const { passport } = authPkg.auth
|
||||||
|
@ -44,14 +44,19 @@ exports.authenticate = async (ctx, next) => {
|
||||||
*/
|
*/
|
||||||
exports.reset = async ctx => {
|
exports.reset = async ctx => {
|
||||||
const { email } = ctx.request.body
|
const { email } = ctx.request.body
|
||||||
try {
|
const configured = await isEmailConfigured()
|
||||||
const user = getGlobalUserByEmail(email)
|
if (!configured) {
|
||||||
if (user) {
|
throw "Please contact your platform administrator, SMTP is not configured."
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const user = await getGlobalUserByEmail(email)
|
||||||
|
sendEmail()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// don't throw any kind of error to the user, this might give away something
|
// 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 => {
|
exports.logout = async ctx => {
|
||||||
|
|
|
@ -45,6 +45,8 @@ const TemplateBindings = {
|
||||||
LOGIN_URL: "loginUrl",
|
LOGIN_URL: "loginUrl",
|
||||||
CURRENT_YEAR: "currentYear",
|
CURRENT_YEAR: "currentYear",
|
||||||
CURRENT_DATE: "currentDate",
|
CURRENT_DATE: "currentDate",
|
||||||
|
RESET_CODE: "resetCode",
|
||||||
|
INVITE_CODE: "inviteCode",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateMetadata = {
|
const TemplateMetadata = {
|
||||||
|
|
|
@ -5,6 +5,7 @@ const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants")
|
||||||
const { getTemplateByPurpose } = require("../constants/templates")
|
const { getTemplateByPurpose } = require("../constants/templates")
|
||||||
const { getSettingsTemplateContext } = require("./templates")
|
const { getSettingsTemplateContext } = require("./templates")
|
||||||
const { processString } = require("@budibase/string-templates")
|
const { processString } = require("@budibase/string-templates")
|
||||||
|
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
const TYPE = TemplateTypes.EMAIL
|
const TYPE = TemplateTypes.EMAIL
|
||||||
|
@ -30,6 +31,24 @@ function createSMTPTransport(config) {
|
||||||
return nodemailer.createTransport(options)
|
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) {
|
async function buildEmail(purpose, email, user) {
|
||||||
// this isn't a full email
|
// this isn't a full email
|
||||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||||
|
@ -47,9 +66,10 @@ async function buildEmail(purpose, email, user) {
|
||||||
styles = styles.contents
|
styles = styles.contents
|
||||||
body = body.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 = {
|
const context = {
|
||||||
...(await getSettingsTemplateContext()),
|
...(await getSettingsTemplateContext(purpose, code)),
|
||||||
email,
|
email,
|
||||||
user: user || {},
|
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 = {
|
const params = {
|
||||||
type: Configs.SMTP,
|
type: Configs.SMTP,
|
||||||
}
|
}
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
params.group = groupId
|
params.group = groupId
|
||||||
}
|
}
|
||||||
let user = {}
|
return determineScopedConfig(db, params)
|
||||||
if (userId) {
|
}
|
||||||
user = db.get(userId)
|
|
||||||
}
|
/**
|
||||||
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) {
|
if (!config) {
|
||||||
throw "Unable to find SMTP configuration"
|
throw "Unable to find SMTP configuration"
|
||||||
}
|
}
|
||||||
|
@ -90,6 +137,11 @@ exports.sendEmail = async (email, purpose, { groupId, userId }) => {
|
||||||
return transport.sendMail(message)
|
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 => {
|
exports.verifyConfig = async config => {
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
await transport.verify()
|
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 CouchDB = require("../db")
|
||||||
const { getConfigParams, StaticDatabases } = require("@budibase/auth").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 { checkSlashesInUrl } = require("./index")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const LOCAL_URL = `http://localhost:${env.PORT}`
|
const LOCAL_URL = `http://localhost:${env.PORT}`
|
||||||
const BASE_COMPANY = "Budibase"
|
const BASE_COMPANY = "Budibase"
|
||||||
|
|
||||||
exports.getSettingsTemplateContext = async () => {
|
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getConfigParams(Configs.SETTINGS, {
|
getConfigParams(Configs.SETTINGS, {
|
||||||
|
@ -18,15 +18,10 @@ exports.getSettingsTemplateContext = async () => {
|
||||||
if (!settings.platformUrl) {
|
if (!settings.platformUrl) {
|
||||||
settings.platformUrl = LOCAL_URL
|
settings.platformUrl = LOCAL_URL
|
||||||
}
|
}
|
||||||
// TODO: need to fully spec out the context
|
|
||||||
const URL = settings.platformUrl
|
const URL = settings.platformUrl
|
||||||
return {
|
const context = {
|
||||||
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
||||||
[TemplateBindings.PLATFORM_URL]: URL,
|
[TemplateBindings.PLATFORM_URL]: URL,
|
||||||
[TemplateBindings.REGISTRATION_URL]: checkSlashesInUrl(
|
|
||||||
`${URL}/registration`
|
|
||||||
),
|
|
||||||
[TemplateBindings.RESET_URL]: checkSlashesInUrl(`${URL}/reset`),
|
|
||||||
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
||||||
[TemplateBindings.DOCS_URL]:
|
[TemplateBindings.DOCS_URL]:
|
||||||
settings.docsUrl || "https://docs.budibase.com/",
|
settings.docsUrl || "https://docs.budibase.com/",
|
||||||
|
@ -34,4 +29,20 @@ exports.getSettingsTemplateContext = async () => {
|
||||||
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
||||||
[TemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
|
[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