Adding a redis client to the auth system, as part of work towards the reset password flow.
This commit is contained in:
parent
e08df4110a
commit
08c158c121
|
@ -59,6 +59,7 @@ services:
|
|||
container_name: budi-redis-dev
|
||||
restart: always
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- "${REDIS_PORT}:6379"
|
||||
volumes:
|
||||
|
|
|
@ -23,6 +23,8 @@ services:
|
|||
LOG_LEVEL: info
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
ENABLE_ANALYTICS: "true"
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
depends_on:
|
||||
- worker-service
|
||||
|
||||
|
@ -43,6 +45,8 @@ services:
|
|||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
SELF_HOST_KEY: ${HOSTING_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
depends_on:
|
||||
- minio-service
|
||||
- couch-init
|
||||
|
@ -100,6 +104,7 @@ services:
|
|||
redis-service:
|
||||
restart: always
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- "${REDIS_PORT}:6379"
|
||||
volumes:
|
||||
|
|
|
@ -12,6 +12,7 @@ MINIO_ACCESS_KEY=budibase
|
|||
MINIO_SECRET_KEY=budibase
|
||||
COUCH_DB_PASSWORD=budibase
|
||||
COUCH_DB_USER=budibase
|
||||
REDIS_PASSWORD=budibase
|
||||
|
||||
# This section contains variables that do not need to be altered under normal circumstances
|
||||
APP_PORT=4002
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"ioredis": "^4.27.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"koa-passport": "^4.1.4",
|
||||
"passport-google-auth": "^1.0.2",
|
||||
|
|
|
@ -2,4 +2,6 @@ module.exports = {
|
|||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
const Redis = require("ioredis")
|
||||
const env = require("../environment")
|
||||
const { addDbPrefix, removeDbPrefix } = require("./utils")
|
||||
|
||||
const CONNECT_TIMEOUT_MS = 10000
|
||||
const SLOT_REFRESH_MS = 2000
|
||||
const CLUSTERED = false
|
||||
|
||||
let CLIENT
|
||||
|
||||
/**
|
||||
* Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise
|
||||
* will return the ioredis client which will be ready to use.
|
||||
* @return {Promise<object>} The ioredis client.
|
||||
*/
|
||||
function init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const [ host, port ] = env.REDIS_URL.split(":")
|
||||
const opts = {
|
||||
connectTimeout: CONNECT_TIMEOUT_MS
|
||||
}
|
||||
if (CLUSTERED) {
|
||||
opts.redisOptions = {}
|
||||
opts.redisOptions.tls = {}
|
||||
opts.redisOptions.password = env.REDIS_PASSWORD
|
||||
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
||||
opts.dnsLookup = (address, callback) => callback(null, address)
|
||||
CLIENT = new Redis.Cluster([ { port, host } ])
|
||||
} else {
|
||||
opts.password = env.REDIS_PASSWORD
|
||||
opts.port = port
|
||||
opts.host = host
|
||||
CLIENT = new Redis(opts)
|
||||
}
|
||||
CLIENT.on("end", err => {
|
||||
reject(err)
|
||||
})
|
||||
CLIENT.on("error", err => {
|
||||
reject(err)
|
||||
})
|
||||
CLIENT.on("connect", () => {
|
||||
resolve(CLIENT)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, takes a redis stream and converts it to a promisified response -
|
||||
* this can only be done with redis streams because they will have an end.
|
||||
* @param stream A redis stream, specifically as this type of stream will have an end.
|
||||
* @return {Promise<object>} The final output of the stream
|
||||
*/
|
||||
function promisifyStream(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputKeys = new Set()
|
||||
stream.on("data", keys => {
|
||||
keys.forEach(key => {
|
||||
outputKeys.add(key)
|
||||
})
|
||||
})
|
||||
stream.on("error", (err) => {
|
||||
reject(err)
|
||||
})
|
||||
stream.on("end", async () => {
|
||||
const keysArray = Array.from(outputKeys)
|
||||
try {
|
||||
let getPromises = []
|
||||
for (let key of keysArray) {
|
||||
getPromises.push(CLIENT.get(key))
|
||||
}
|
||||
const jsonArray = await Promise.all(getPromises)
|
||||
resolve(keysArray.map(key => ({
|
||||
key: removeDbPrefix(key),
|
||||
value: JSON.parse(jsonArray.shift()),
|
||||
})))
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class RedisWrapper {
|
||||
constructor(db) {
|
||||
this._db = db
|
||||
}
|
||||
|
||||
async init() {
|
||||
this._client = await init()
|
||||
return this
|
||||
}
|
||||
|
||||
async scan() {
|
||||
const db = this._db, client = this._client
|
||||
let stream
|
||||
if (CLUSTERED) {
|
||||
let node = client.nodes("master")
|
||||
stream = node[0].scanStream({match: db + "-*", count: 100})
|
||||
|
||||
} else {
|
||||
stream = client.scanStream({match: db + "-*", count: 100})
|
||||
}
|
||||
return promisifyStream(stream)
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
const db = this._db, client = this._client
|
||||
let response = await client.get(addDbPrefix(db, key))
|
||||
// overwrite the prefixed key
|
||||
if (response != null && response.key) {
|
||||
response.key = key
|
||||
}
|
||||
return JSON.parse(response)
|
||||
}
|
||||
|
||||
async store(key, value, expirySeconds = null) {
|
||||
const db = this._db, client = this._client
|
||||
if (typeof(value) === "object") {
|
||||
value = JSON.stringify(value)
|
||||
}
|
||||
const prefixedKey = addDbPrefix(db, key)
|
||||
await client.set(prefixedKey, value)
|
||||
if (expirySeconds) {
|
||||
await client.expire(prefixedKey, expirySeconds)
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key) {
|
||||
const db = this._db, client = this._client
|
||||
await client.del(addDbPrefix(db, key))
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const db = this._db
|
||||
let items = await this.scan(db)
|
||||
await Promise.all(items.map(obj => this.delete(db, obj.key)))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RedisWrapper
|
|
@ -0,0 +1,20 @@
|
|||
const SEPARATOR = "-"
|
||||
|
||||
exports.Databases = {
|
||||
PW_RESETS: "pwReset",
|
||||
}
|
||||
|
||||
exports.addDbPrefix = (db, key) => {
|
||||
return `${db}${SEPARATOR}${key}`
|
||||
}
|
||||
|
||||
exports.removeDbPrefix = key => {
|
||||
let parts = key.split(SEPARATOR)
|
||||
if (parts.length >= 2) {
|
||||
parts.shift()
|
||||
return parts.join(SEPARATOR)
|
||||
} else {
|
||||
// return the only part
|
||||
return parts[0]
|
||||
}
|
||||
}
|
|
@ -73,6 +73,11 @@ caseless@~0.12.0:
|
|||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||
|
||||
cluster-key-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
||||
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
|
@ -92,11 +97,23 @@ dashdash@^1.12.0:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
debug@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
denque@^1.1.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
|
||||
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
|
@ -216,6 +233,22 @@ http-signature@~1.2.0:
|
|||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
ioredis@^4.27.1:
|
||||
version "4.27.1"
|
||||
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.1.tgz#4ef947b455a1b995baa4b0d7e2c4e4f75f746421"
|
||||
integrity sha512-PaFNFeBbOcEYHXAdrJuy7uesJcyvzStTM1aYMchTuky+VgKqDbXhnTJHaDsjAwcTwPx8Asatx+l2DW8zZ2xlsQ==
|
||||
dependencies:
|
||||
cluster-key-slot "^1.1.0"
|
||||
debug "^4.3.1"
|
||||
denque "^1.1.0"
|
||||
lodash.defaults "^4.2.0"
|
||||
lodash.flatten "^4.4.0"
|
||||
p-map "^2.1.0"
|
||||
redis-commands "1.7.0"
|
||||
redis-errors "^1.2.0"
|
||||
redis-parser "^3.0.0"
|
||||
standard-as-callback "^2.1.0"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
@ -296,6 +329,16 @@ koa-passport@^4.1.4:
|
|||
dependencies:
|
||||
passport "^0.4.0"
|
||||
|
||||
lodash.defaults@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
||||
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
|
||||
|
||||
lodash.flatten@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
|
@ -358,6 +401,11 @@ mime@^1.4.1:
|
|||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
|
@ -378,6 +426,11 @@ oauth@0.9.x:
|
|||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
||||
|
||||
p-map@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||
|
||||
passport-google-auth@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938"
|
||||
|
@ -481,6 +534,23 @@ qs@~6.5.2:
|
|||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
redis-commands@1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
|
||||
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
|
||||
|
||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
|
||||
|
||||
redis-parser@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
|
||||
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
|
||||
dependencies:
|
||||
redis-errors "^1.0.0"
|
||||
|
||||
request@^2.72.0, request@^2.74.0:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
|
@ -537,6 +607,11 @@ sshpk@^1.7.0:
|
|||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
standard-as-callback@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
|
||||
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
|
||||
|
||||
string-template@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
|
||||
|
|
|
@ -20,6 +20,7 @@ MINIO_ACCESS_KEY=${randomString.generate()}
|
|||
MINIO_SECRET_KEY=${randomString.generate()}
|
||||
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||
COUCH_DB_USER=${randomString.generate()}
|
||||
REDIS_PASSWORD=${randomString.generate()}
|
||||
|
||||
# This section contains variables that do not need to be altered under normal circumstances
|
||||
APP_PORT=4002
|
||||
|
|
|
@ -32,6 +32,8 @@ module.exports = {
|
|||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
USE_QUOTAS: process.env.USE_QUOTAS,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
// environment
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||
|
|
|
@ -1,82 +1,8 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const { StaticDatabases, determineScopedConfig } = require("@budibase/auth").db
|
||||
const {
|
||||
EmailTemplatePurpose,
|
||||
TemplateTypes,
|
||||
Configs,
|
||||
} = require("../../../constants")
|
||||
const { getTemplateByPurpose } = require("../../../constants/templates")
|
||||
const { getSettingsTemplateContext } = require("../../../utilities/templates")
|
||||
const { processString } = require("@budibase/string-templates")
|
||||
const { createSMTPTransport } = require("../../../utilities/email")
|
||||
|
||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||
const TYPE = TemplateTypes.EMAIL
|
||||
|
||||
const FULL_EMAIL_PURPOSES = [
|
||||
EmailTemplatePurpose.INVITATION,
|
||||
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||
EmailTemplatePurpose.WELCOME,
|
||||
]
|
||||
|
||||
async function buildEmail(purpose, email, user) {
|
||||
// this isn't a full email
|
||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||
throw `Unable to build an email of type ${purpose}`
|
||||
}
|
||||
let [base, styles, body] = await Promise.all([
|
||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES),
|
||||
getTemplateByPurpose(TYPE, purpose),
|
||||
])
|
||||
if (!base || !styles || !body) {
|
||||
throw "Unable to build email, missing base components"
|
||||
}
|
||||
base = base.contents
|
||||
styles = styles.contents
|
||||
body = body.contents
|
||||
|
||||
// TODO: need to extend the context as much as possible
|
||||
const context = {
|
||||
...(await getSettingsTemplateContext()),
|
||||
email,
|
||||
user: user || {},
|
||||
}
|
||||
|
||||
body = await processString(body, context)
|
||||
styles = await processString(styles, context)
|
||||
// this should now be the complete email HTML
|
||||
return processString(base, {
|
||||
...context,
|
||||
styles,
|
||||
body,
|
||||
})
|
||||
}
|
||||
const { sendEmail } = require("../../../utilities/email")
|
||||
|
||||
exports.sendEmail = async ctx => {
|
||||
const { groupId, email, userId, purpose } = ctx.request.body
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const params = {}
|
||||
if (groupId) {
|
||||
params.group = groupId
|
||||
}
|
||||
params.type = Configs.SMTP
|
||||
let user = {}
|
||||
if (userId) {
|
||||
user = db.get(userId)
|
||||
}
|
||||
const config = await determineScopedConfig(db, params)
|
||||
if (!config) {
|
||||
ctx.throw(400, "Unable to find SMTP configuration")
|
||||
}
|
||||
const transport = createSMTPTransport(config)
|
||||
const message = {
|
||||
from: config.from,
|
||||
subject: config.subject,
|
||||
to: email,
|
||||
html: await buildEmail(purpose, email, user),
|
||||
}
|
||||
const response = await transport.sendMail(message)
|
||||
const response = await sendEmail(email, purpose, { groupId, userId })
|
||||
ctx.body = {
|
||||
...response,
|
||||
message: `Email sent to ${email}.`,
|
||||
|
|
|
@ -2,31 +2,36 @@ const authPkg = require("@budibase/auth")
|
|||
const { google } = require("@budibase/auth/src/middleware")
|
||||
const { Configs } = require("../../constants")
|
||||
const CouchDB = require("../../db")
|
||||
const { clearCookie } = authPkg.utils
|
||||
const { sendEmail } = require("../../utilities/email")
|
||||
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
||||
const { Cookies } = authPkg.constants
|
||||
const { passport } = authPkg.auth
|
||||
|
||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||
|
||||
function authInternal(ctx, user, err = null) {
|
||||
if (err) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
if (!user) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
})
|
||||
}
|
||||
|
||||
exports.authenticate = async (ctx, next) => {
|
||||
return passport.authenticate("local", async (err, user) => {
|
||||
if (err) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
if (!user) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
})
|
||||
authInternal(ctx, err, user)
|
||||
|
||||
delete user.token
|
||||
|
||||
|
@ -34,6 +39,22 @@ exports.authenticate = async (ctx, next) => {
|
|||
})(ctx, next)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the user password, used as part of a forgotten password flow.
|
||||
*/
|
||||
exports.reset = async ctx => {
|
||||
const { email } = ctx.request.body
|
||||
try {
|
||||
const user = getGlobalUserByEmail(email)
|
||||
if (user) {
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
// don't throw any kind of error to the user, this might give away something
|
||||
}
|
||||
ctx.body = {}
|
||||
}
|
||||
|
||||
exports.logout = async ctx => {
|
||||
clearCookie(ctx, Cookies.Auth)
|
||||
ctx.body = { message: "User logged out" }
|
||||
|
@ -69,23 +90,7 @@ exports.googleAuth = async (ctx, next) => {
|
|||
strategy,
|
||||
{ successRedirect: "/", failureRedirect: "/error" },
|
||||
async (err, user) => {
|
||||
if (err) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
if (!user) {
|
||||
return ctx.throw(403, "Unauthorized")
|
||||
}
|
||||
|
||||
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
})
|
||||
authInternal(ctx, user, err)
|
||||
|
||||
ctx.redirect("/")
|
||||
}
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
const Router = require("@koa/router")
|
||||
const authController = require("../controllers/auth")
|
||||
const joiValidator = require("../../middleware/joi-validator")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
||||
function buildAuthValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).required().unknown(false))
|
||||
}
|
||||
|
||||
function buildResetValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
email: Joi.string().required(),
|
||||
}).required().unknown(false))
|
||||
}
|
||||
|
||||
router
|
||||
.post("/api/admin/auth", authController.authenticate)
|
||||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate)
|
||||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset)
|
||||
.post("/api/admin/auth/logout", authController.logout)
|
||||
.get("/api/admin/auth/google", authController.googlePreAuth)
|
||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||
.post("/api/admin/auth/logout", authController.logout)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -26,6 +26,8 @@ module.exports = {
|
|||
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
/* TODO: to remove - once deployment removed */
|
||||
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
|
||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
const nodemailer = require("nodemailer")
|
||||
const CouchDB = require("../db")
|
||||
const { StaticDatabases, determineScopedConfig } = require("@budibase/auth").db
|
||||
const {
|
||||
EmailTemplatePurpose,
|
||||
TemplateTypes,
|
||||
Configs,
|
||||
} = require("../constants")
|
||||
const { getTemplateByPurpose } = require("../constants/templates")
|
||||
const { getSettingsTemplateContext } = require("./templates")
|
||||
const { processString } = require("@budibase/string-templates")
|
||||
|
||||
exports.createSMTPTransport = config => {
|
||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||
const TYPE = TemplateTypes.EMAIL
|
||||
|
||||
const FULL_EMAIL_PURPOSES = [
|
||||
EmailTemplatePurpose.INVITATION,
|
||||
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||
EmailTemplatePurpose.WELCOME,
|
||||
]
|
||||
|
||||
function createSMTPTransport(config) {
|
||||
const options = {
|
||||
port: config.port,
|
||||
host: config.host,
|
||||
|
@ -15,7 +34,68 @@ exports.createSMTPTransport = config => {
|
|||
return nodemailer.createTransport(options)
|
||||
}
|
||||
|
||||
async function buildEmail(purpose, email, user) {
|
||||
// this isn't a full email
|
||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||
throw `Unable to build an email of type ${purpose}`
|
||||
}
|
||||
let [base, styles, body] = await Promise.all([
|
||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES),
|
||||
getTemplateByPurpose(TYPE, purpose),
|
||||
])
|
||||
if (!base || !styles || !body) {
|
||||
throw "Unable to build email, missing base components"
|
||||
}
|
||||
base = base.contents
|
||||
styles = styles.contents
|
||||
body = body.contents
|
||||
|
||||
// TODO: need to extend the context as much as possible
|
||||
const context = {
|
||||
...(await getSettingsTemplateContext()),
|
||||
email,
|
||||
user: user || {},
|
||||
}
|
||||
|
||||
body = await processString(body, context)
|
||||
styles = await processString(styles, context)
|
||||
// this should now be the complete email HTML
|
||||
return processString(base, {
|
||||
...context,
|
||||
styles,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
exports.sendEmail = async (email, purpose, { groupId, userId }) => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const params = {
|
||||
type: Configs.SMTP,
|
||||
}
|
||||
if (groupId) {
|
||||
params.group = groupId
|
||||
}
|
||||
let user = {}
|
||||
if (userId) {
|
||||
user = db.get(userId)
|
||||
}
|
||||
const config = await determineScopedConfig(db, params)
|
||||
if (!config) {
|
||||
throw "Unable to find SMTP configuration"
|
||||
}
|
||||
const transport = createSMTPTransport(config)
|
||||
const message = {
|
||||
from: config.from,
|
||||
subject: config.subject,
|
||||
to: email,
|
||||
html: await buildEmail(purpose, email, user),
|
||||
}
|
||||
return transport.sendMail(message)
|
||||
}
|
||||
|
||||
|
||||
exports.verifyConfig = async config => {
|
||||
const transport = exports.createSMTPTransport(config)
|
||||
const transport = createSMTPTransport(config)
|
||||
await transport.verify()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue