Finalising the usage of redis in the password reset and invitation systems.
This commit is contained in:
parent
f781e8b350
commit
f1650105f4
|
@ -105,6 +105,12 @@ exports.isClient = ctx => {
|
||||||
return ctx.headers["x-budibase-type"] === "client"
|
return ctx.headers["x-budibase-type"] === "client"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an email address this will use a view to search through
|
||||||
|
* all the users to find one with this email address.
|
||||||
|
* @param {string} email the email to lookup the user by.
|
||||||
|
* @return {Promise<object|null>}
|
||||||
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
exports.getGlobalUserByEmail = async email => {
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
const db = getDB(StaticDatabases.GLOBAL.name)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
const authPkg = require("@budibase/auth")
|
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, EmailTemplatePurpose } = require("../../../constants")
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
const { clearCookie, getGlobalUserByEmail, hash } = authPkg.utils
|
||||||
const { Cookies } = authPkg.constants
|
const { Cookies } = authPkg.constants
|
||||||
const { passport } = authPkg.auth
|
const { passport } = authPkg.auth
|
||||||
|
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
@ -50,18 +51,36 @@ exports.reset = async ctx => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const user = await getGlobalUserByEmail(email)
|
const user = await getGlobalUserByEmail(email)
|
||||||
sendEmail()
|
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user })
|
||||||
} 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.",
|
message: "Please check your email for a reset link.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the user password update if the provided reset code is valid.
|
||||||
|
*/
|
||||||
|
exports.resetUpdate = async ctx => {
|
||||||
|
const { resetCode, password } = ctx.request.body
|
||||||
|
const userId = await checkResetPasswordCode(resetCode)
|
||||||
|
if (!userId) {
|
||||||
|
throw "Cannot reset password."
|
||||||
|
}
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const user = await db.get(userId)
|
||||||
|
user.password = await hash(password)
|
||||||
|
await db.put(user)
|
||||||
|
ctx.body = {
|
||||||
|
message: "password reset successfully.",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.logout = async ctx => {
|
exports.logout = async ctx => {
|
||||||
clearCookie(ctx, Cookies.Auth)
|
clearCookie(ctx, Cookies.Auth)
|
||||||
ctx.body = { message: "User logged out" }
|
ctx.body = { message: "User logged out." }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,7 @@ const {
|
||||||
} = require("@budibase/auth").db
|
} = require("@budibase/auth").db
|
||||||
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||||
const { UserStatus } = require("../../../constants")
|
const { UserStatus } = require("../../../constants")
|
||||||
|
const { checkResetPasswordCode, checkInviteCode } = require("../../../utilities/redis")
|
||||||
|
|
||||||
const FIRST_USER_EMAIL = "test@test.com"
|
const FIRST_USER_EMAIL = "test@test.com"
|
||||||
const FIRST_USER_PASSWORD = "test"
|
const FIRST_USER_PASSWORD = "test"
|
||||||
|
@ -42,7 +43,7 @@ exports.save = async ctx => {
|
||||||
user.status = UserStatus.ACTIVE
|
user.status = UserStatus.ACTIVE
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await db.post({
|
const response = await db.put({
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
...user,
|
...user,
|
||||||
})
|
})
|
||||||
|
@ -121,3 +122,20 @@ exports.find = async ctx => {
|
||||||
}
|
}
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.invite = async ctx => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.inviteAccept = async ctx => {
|
||||||
|
const { inviteCode } = ctx.request.body
|
||||||
|
const email = await checkInviteCode(inviteCode)
|
||||||
|
if (!email) {
|
||||||
|
throw "Unable to create new user, invitation invalid."
|
||||||
|
}
|
||||||
|
// redirect the request
|
||||||
|
delete ctx.request.body.inviteCode
|
||||||
|
ctx.request.body.email = email
|
||||||
|
// this will flesh out the body response
|
||||||
|
await exports.save(ctx)
|
||||||
|
}
|
||||||
|
|
|
@ -4,11 +4,15 @@ const zlib = require("zlib")
|
||||||
const { routes } = require("./routes")
|
const { routes } = require("./routes")
|
||||||
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||||
|
|
||||||
const NO_AUTH_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
{
|
{
|
||||||
route: "/api/admin/users/first",
|
route: "/api/admin/users/first",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/users/invite/accept",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
route: "/api/admin/auth",
|
route: "/api/admin/auth",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -21,10 +25,13 @@ const NO_AUTH_ENDPOINTS = [
|
||||||
route: "/api/admin/auth/google/callback",
|
route: "/api/admin/auth/google/callback",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/reset",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
compress({
|
compress({
|
||||||
|
@ -39,7 +46,7 @@ router
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (!ctx.isAuthenticated) {
|
if (!ctx.isAuthenticated) {
|
||||||
|
|
|
@ -20,9 +20,18 @@ function buildResetValidation() {
|
||||||
}).required().unknown(false))
|
}).required().unknown(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildResetUpdateValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
resetCode: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}).required().unknown(false))
|
||||||
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
|
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
|
||||||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
|
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
|
||||||
|
.post("/api/admin/auth/reset/update", buildResetUpdateValidation(), authController.resetUpdate)
|
||||||
.post("/api/admin/auth/logout", authController.logout)
|
.post("/api/admin/auth/logout", authController.logout)
|
||||||
.get("/api/admin/auth/google", authController.googlePreAuth)
|
.get("/api/admin/auth/google", authController.googlePreAuth)
|
||||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||||
|
|
|
@ -21,7 +21,22 @@ function buildUserSaveValidation() {
|
||||||
.pattern(/.*/, Joi.string())
|
.pattern(/.*/, Joi.string())
|
||||||
.required()
|
.required()
|
||||||
.unknown(true)
|
.unknown(true)
|
||||||
}).required().unknown(true).optional())
|
}).required().unknown(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInviteValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
email: Joi.string().required(),
|
||||||
|
}).required())
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInviteAcceptValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
inviteCode: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
|
@ -30,5 +45,7 @@ router
|
||||||
.post("/api/admin/users/first", controller.firstUser)
|
.post("/api/admin/users/first", controller.firstUser)
|
||||||
.delete("/api/admin/users/:id", controller.destroy)
|
.delete("/api/admin/users/:id", controller.destroy)
|
||||||
.get("/api/admin/users/:id", controller.find)
|
.get("/api/admin/users/:id", controller.find)
|
||||||
|
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
||||||
|
.post("/api/admin/users/invite/accept", buildInviteAcceptValidation(), controller.inviteAccept)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -116,8 +116,8 @@ exports.isEmailConfigured = async (groupId = null) => {
|
||||||
* send an email using it.
|
* send an email using it.
|
||||||
* @param {string} email The email address to send to.
|
* @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} 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 {string|undefined} 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.
|
* @param {object|undefined} user 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
|
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
||||||
* nodemailer response.
|
* nodemailer response.
|
||||||
*/
|
*/
|
||||||
|
@ -125,7 +125,7 @@ exports.sendEmail = async (email, purpose, { groupId, user }) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const config = await getSmtpConfiguration(db, groupId)
|
const config = await getSmtpConfiguration(db, groupId)
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw "Unable to find SMTP configuration"
|
throw "Unable to find SMTP configuration."
|
||||||
}
|
}
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
const message = {
|
const message = {
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
const { Client, utils } = require("@budibase/auth").redis
|
const { Client, utils } = require("@budibase/auth").redis
|
||||||
const { newid } = require("@budibase/auth").utils
|
const { newid } = require("@budibase/auth").utils
|
||||||
|
|
||||||
const EXPIRE_TOKEN_SECONDS = 3600
|
function getExpirySecondsForDB(db) {
|
||||||
|
switch (db) {
|
||||||
|
case utils.Databases.PW_RESETS:
|
||||||
|
// a hour
|
||||||
|
return 3600
|
||||||
|
case utils.Databases.INVITATIONS:
|
||||||
|
// a day
|
||||||
|
return 86400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getClient(db) {
|
async function getClient(db) {
|
||||||
return await new Client(db).init()
|
return await new Client(db).init()
|
||||||
|
@ -10,11 +19,24 @@ async function getClient(db) {
|
||||||
async function writeACode(db, value) {
|
async function writeACode(db, value) {
|
||||||
const client = await getClient(db)
|
const client = await getClient(db)
|
||||||
const code = newid()
|
const code = newid()
|
||||||
await client.store(code, value, EXPIRE_TOKEN_SECONDS)
|
await client.store(code, value, getExpirySecondsForDB(db))
|
||||||
client.finish()
|
client.finish()
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getACode(db, code, deleteCode = true) {
|
||||||
|
const client = await getClient(db)
|
||||||
|
const value = await client.get(code)
|
||||||
|
if (!value) {
|
||||||
|
throw "Invalid code."
|
||||||
|
}
|
||||||
|
if (deleteCode) {
|
||||||
|
await client.delete(code)
|
||||||
|
}
|
||||||
|
client.finish()
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a user ID this will store a code (that is returned) for an hour in redis.
|
* 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).
|
* The user can then return this code for resetting their password (through their reset link).
|
||||||
|
@ -28,17 +50,15 @@ exports.getResetPasswordCode = async userId => {
|
||||||
/**
|
/**
|
||||||
* Given a reset code this will lookup to redis, check if the code is valid and delete if required.
|
* 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 {string} resetCode The code provided via the email link.
|
||||||
* @param {boolean} deleteCode If the code is used/finished with this will delete it.
|
* @param {boolean} deleteCode If the code is used/finished with this will delete it - defaults to true.
|
||||||
* @return {Promise<string>} returns the user ID if it is found
|
* @return {Promise<string>} returns the user ID if it is found
|
||||||
*/
|
*/
|
||||||
exports.checkResetPasswordCode = async (resetCode, deleteCode = false) => {
|
exports.checkResetPasswordCode = async (resetCode, deleteCode = true) => {
|
||||||
const client = await getClient(utils.Databases.PW_RESETS)
|
try {
|
||||||
const userId = await client.get(resetCode)
|
return getACode(utils.Databases.PW_RESETS, resetCode, deleteCode)
|
||||||
if (deleteCode) {
|
} catch (err) {
|
||||||
await client.delete(resetCode)
|
throw "Provided information is not valid, cannot reset password - please try again."
|
||||||
}
|
}
|
||||||
client.finish()
|
|
||||||
return userId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,3 +69,17 @@ exports.checkResetPasswordCode = async (resetCode, deleteCode = false) => {
|
||||||
exports.getInviteCode = async email => {
|
exports.getInviteCode = async email => {
|
||||||
return writeACode(utils.Databases.INVITATIONS, email)
|
return writeACode(utils.Databases.INVITATIONS, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the provided invite code is valid - will return the email address of user that was invited.
|
||||||
|
* @param {string} inviteCode the invite code that was provided as part of the link.
|
||||||
|
* @param {boolean} deleteCode whether or not the code should be deleted after retrieval - defaults to true.
|
||||||
|
* @return {Promise<string>} If the code is valid then an email address will be returned.
|
||||||
|
*/
|
||||||
|
exports.checkInviteCode = async (inviteCode, deleteCode = true) => {
|
||||||
|
try {
|
||||||
|
return getACode(utils.Databases.INVITATIONS, inviteCode, deleteCode)
|
||||||
|
} catch (err) {
|
||||||
|
throw "Invitation is not valid or has expired, please request a new one."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue