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
|
container_name: budi-redis-dev
|
||||||
restart: always
|
restart: always
|
||||||
image: redis
|
image: redis
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT}:6379"
|
- "${REDIS_PORT}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -23,6 +23,8 @@ services:
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||||
ENABLE_ANALYTICS: "true"
|
ENABLE_ANALYTICS: "true"
|
||||||
|
REDIS_URL: redis-service:6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-service
|
- worker-service
|
||||||
|
|
||||||
|
@ -43,6 +45,8 @@ services:
|
||||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
SELF_HOST_KEY: ${HOSTING_KEY}
|
SELF_HOST_KEY: ${HOSTING_KEY}
|
||||||
|
REDIS_URL: redis-service:6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
- minio-service
|
- minio-service
|
||||||
- couch-init
|
- couch-init
|
||||||
|
@ -100,6 +104,7 @@ services:
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: always
|
restart: always
|
||||||
image: redis
|
image: redis
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT}:6379"
|
- "${REDIS_PORT}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -12,6 +12,7 @@ MINIO_ACCESS_KEY=budibase
|
||||||
MINIO_SECRET_KEY=budibase
|
MINIO_SECRET_KEY=budibase
|
||||||
COUCH_DB_PASSWORD=budibase
|
COUCH_DB_PASSWORD=budibase
|
||||||
COUCH_DB_USER=budibase
|
COUCH_DB_USER=budibase
|
||||||
|
REDIS_PASSWORD=budibase
|
||||||
|
|
||||||
# This section contains variables that do not need to be altered under normal circumstances
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"ioredis": "^4.27.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa-passport": "^4.1.4",
|
"koa-passport": "^4.1.4",
|
||||||
"passport-google-auth": "^1.0.2",
|
"passport-google-auth": "^1.0.2",
|
||||||
|
|
|
@ -2,4 +2,6 @@ module.exports = {
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
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"
|
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
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:
|
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
@ -92,11 +97,23 @@ dashdash@^1.12.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
assert-plus "^1.0.0"
|
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:
|
delayed-stream@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
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:
|
ecc-jsbn@~0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
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"
|
jsprim "^1.2.2"
|
||||||
sshpk "^1.7.0"
|
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:
|
is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
|
@ -296,6 +329,16 @@ koa-passport@^4.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
passport "^0.4.0"
|
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:
|
lodash.includes@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
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"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
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:
|
ms@^2.1.1:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
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"
|
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
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:
|
passport-google-auth@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938"
|
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"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
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:
|
request@^2.72.0, request@^2.74.0:
|
||||||
version "2.88.2"
|
version "2.88.2"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
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"
|
safer-buffer "^2.0.2"
|
||||||
tweetnacl "~0.14.0"
|
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:
|
string-template@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
|
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()}
|
MINIO_SECRET_KEY=${randomString.generate()}
|
||||||
COUCH_DB_PASSWORD=${randomString.generate()}
|
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||||
COUCH_DB_USER=${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
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
|
|
@ -32,6 +32,8 @@ module.exports = {
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
USE_QUOTAS: process.env.USE_QUOTAS,
|
USE_QUOTAS: process.env.USE_QUOTAS,
|
||||||
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
// environment
|
// environment
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||||
|
|
|
@ -1,82 +1,8 @@
|
||||||
const CouchDB = require("../../../db")
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.sendEmail = async ctx => {
|
exports.sendEmail = async ctx => {
|
||||||
const { groupId, email, userId, purpose } = ctx.request.body
|
const { groupId, email, userId, purpose } = ctx.request.body
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const response = await sendEmail(email, purpose, { groupId, userId })
|
||||||
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)
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...response,
|
...response,
|
||||||
message: `Email sent to ${email}.`,
|
message: `Email sent to ${email}.`,
|
||||||
|
|
|
@ -2,31 +2,36 @@ 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 { clearCookie } = authPkg.utils
|
const { sendEmail } = require("../../utilities/email")
|
||||||
|
const { clearCookie, getGlobalUserByEmail } = authPkg.utils
|
||||||
const { Cookies } = authPkg.constants
|
const { Cookies } = authPkg.constants
|
||||||
const { passport } = authPkg.auth
|
const { passport } = authPkg.auth
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
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) => {
|
exports.authenticate = async (ctx, next) => {
|
||||||
return passport.authenticate("local", async (err, user) => {
|
return passport.authenticate("local", async (err, user) => {
|
||||||
if (err) {
|
authInternal(ctx, err, user)
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
delete user.token
|
delete user.token
|
||||||
|
|
||||||
|
@ -34,6 +39,22 @@ exports.authenticate = async (ctx, next) => {
|
||||||
})(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 => {
|
exports.logout = async ctx => {
|
||||||
clearCookie(ctx, Cookies.Auth)
|
clearCookie(ctx, Cookies.Auth)
|
||||||
ctx.body = { message: "User logged out" }
|
ctx.body = { message: "User logged out" }
|
||||||
|
@ -69,23 +90,7 @@ exports.googleAuth = async (ctx, next) => {
|
||||||
strategy,
|
strategy,
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err, user) => {
|
async (err, user) => {
|
||||||
if (err) {
|
authInternal(ctx, user, 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.redirect("/")
|
ctx.redirect("/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const authController = require("../controllers/auth")
|
const authController = require("../controllers/auth")
|
||||||
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
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
|
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", authController.googlePreAuth)
|
||||||
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||||
.post("/api/admin/auth/logout", authController.logout)
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -26,6 +26,8 @@ module.exports = {
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
/* TODO: to remove - once deployment removed */
|
/* TODO: to remove - once deployment removed */
|
||||||
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
|
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
||||||
|
|
|
@ -1,6 +1,25 @@
|
||||||
const nodemailer = require("nodemailer")
|
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 = {
|
const options = {
|
||||||
port: config.port,
|
port: config.port,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
|
@ -15,7 +34,68 @@ exports.createSMTPTransport = config => {
|
||||||
return nodemailer.createTransport(options)
|
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 => {
|
exports.verifyConfig = async config => {
|
||||||
const transport = exports.createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
await transport.verify()
|
await transport.verify()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue