Finalising the usage of redis in the password reset and invitation systems.
This commit is contained in:
parent
fc01bdaac9
commit
7bc3514fc1
|
@ -105,6 +105,12 @@ exports.isClient = ctx => {
|
|||
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 => {
|
||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
||||
try {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
const authPkg = require("@budibase/auth")
|
||||
const { google } = require("@budibase/auth/src/middleware")
|
||||
const { Configs } = require("../../../constants")
|
||||
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||
const CouchDB = require("../../../db")
|
||||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
||||
const { clearCookie, getGlobalUserByEmail, hash } = authPkg.utils
|
||||
const { Cookies } = authPkg.constants
|
||||
const { passport } = authPkg.auth
|
||||
const { checkResetPasswordCode } = require("../../../utilities/redis")
|
||||
|
||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||
|
||||
|
@ -50,18 +51,36 @@ exports.reset = async ctx => {
|
|||
}
|
||||
try {
|
||||
const user = await getGlobalUserByEmail(email)
|
||||
sendEmail()
|
||||
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user })
|
||||
} catch (err) {
|
||||
// don't throw any kind of error to the user, this might give away something
|
||||
}
|
||||
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 => {
|
||||
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
|
||||
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||
const { UserStatus } = require("../../../constants")
|
||||
const { checkResetPasswordCode, checkInviteCode } = require("../../../utilities/redis")
|
||||
|
||||
const FIRST_USER_EMAIL = "test@test.com"
|
||||
const FIRST_USER_PASSWORD = "test"
|
||||
|
@ -42,7 +43,7 @@ exports.save = async ctx => {
|
|||
user.status = UserStatus.ACTIVE
|
||||
}
|
||||
try {
|
||||
const response = await db.post({
|
||||
const response = await db.put({
|
||||
password: hashedPassword,
|
||||
...user,
|
||||
})
|
||||
|
@ -121,3 +122,20 @@ exports.find = async ctx => {
|
|||
}
|
||||
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 { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||
|
||||
const NO_AUTH_ENDPOINTS = [
|
||||
const PUBLIC_ENDPOINTS = [
|
||||
{
|
||||
route: "/api/admin/users/first",
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
route: "/api/admin/users/invite/accept",
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
route: "/api/admin/auth",
|
||||
method: "POST",
|
||||
|
@ -21,10 +25,13 @@ const NO_AUTH_ENDPOINTS = [
|
|||
route: "/api/admin/auth/google/callback",
|
||||
method: "GET",
|
||||
},
|
||||
{
|
||||
route: "/api/admin/auth/reset",
|
||||
method: "POST",
|
||||
},
|
||||
]
|
||||
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.use(
|
||||
compress({
|
||||
|
@ -39,7 +46,7 @@ router
|
|||
})
|
||||
)
|
||||
.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)
|
||||
.use((ctx, next) => {
|
||||
if (!ctx.isAuthenticated) {
|
||||
|
|
|
@ -20,9 +20,18 @@ function buildResetValidation() {
|
|||
}).required().unknown(false))
|
||||
}
|
||||
|
||||
function buildResetUpdateValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
resetCode: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).required().unknown(false))
|
||||
}
|
||||
|
||||
router
|
||||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
|
||||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
|
||||
.post("/api/admin/auth/reset/update", buildResetUpdateValidation(), authController.resetUpdate)
|
||||
.post("/api/admin/auth/logout", authController.logout)
|
||||
.get("/api/admin/auth/google", authController.googlePreAuth)
|
||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||
|
|
|
@ -21,7 +21,22 @@ function buildUserSaveValidation() {
|
|||
.pattern(/.*/, Joi.string())
|
||||
.required()
|
||||
.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
|
||||
|
@ -30,5 +45,7 @@ router
|
|||
.post("/api/admin/users/first", controller.firstUser)
|
||||
.delete("/api/admin/users/:id", controller.destroy)
|
||||
.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
|
||||
|
|
|
@ -116,8 +116,8 @@ exports.isEmailConfigured = async (groupId = null) => {
|
|||
* 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.
|
||||
* @param {string|undefined} groupId If finer grain controls being used then this will lookup config for group.
|
||||
* @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
|
||||
* nodemailer response.
|
||||
*/
|
||||
|
@ -125,7 +125,7 @@ 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"
|
||||
throw "Unable to find SMTP configuration."
|
||||
}
|
||||
const transport = createSMTPTransport(config)
|
||||
const message = {
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
const { Client, utils } = require("@budibase/auth").redis
|
||||
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) {
|
||||
return await new Client(db).init()
|
||||
|
@ -10,11 +19,24 @@ async function getClient(db) {
|
|||
async function writeACode(db, value) {
|
||||
const client = await getClient(db)
|
||||
const code = newid()
|
||||
await client.store(code, value, EXPIRE_TOKEN_SECONDS)
|
||||
await client.store(code, value, getExpirySecondsForDB(db))
|
||||
client.finish()
|
||||
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.
|
||||
* 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.
|
||||
* @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
|
||||
*/
|
||||
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)
|
||||
exports.checkResetPasswordCode = async (resetCode, deleteCode = true) => {
|
||||
try {
|
||||
return getACode(utils.Databases.PW_RESETS, resetCode, deleteCode)
|
||||
} catch (err) {
|
||||
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 => {
|
||||
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