From 08c158c1210402acefd4f1784e3207c16e33cc19 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 27 Apr 2021 17:29:05 +0100 Subject: [PATCH] Adding a redis client to the auth system, as part of work towards the reset password flow. --- hosting/docker-compose.dev.yaml | 1 + hosting/docker-compose.yaml | 5 + hosting/hosting.properties | 1 + packages/auth/package.json | 1 + packages/auth/src/environment.js | 2 + packages/auth/src/redis/index.js | 140 ++++++++++++++++++ packages/auth/src/redis/utils.js | 20 +++ packages/auth/yarn.lock | 75 ++++++++++ packages/cli/src/hosting/makeEnv.js | 1 + packages/server/src/environment.js | 2 + .../worker/src/api/controllers/admin/email.js | 78 +--------- packages/worker/src/api/controllers/auth.js | 75 +++++----- packages/worker/src/api/routes/auth.js | 22 ++- packages/worker/src/environment.js | 2 + packages/worker/src/utilities/email.js | 84 ++++++++++- 15 files changed, 394 insertions(+), 115 deletions(-) create mode 100644 packages/auth/src/redis/index.js create mode 100644 packages/auth/src/redis/utils.js diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index 9b4c353981..3b99ef796c 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -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: diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 5e21cc9efd..e062f0590e 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -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: diff --git a/hosting/hosting.properties b/hosting/hosting.properties index 138e66d629..4297ec60a1 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -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 diff --git a/packages/auth/package.json b/packages/auth/package.json index b4f4b1cb33..2de0bd7a3e 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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", diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index 3a5c81ea8b..4f195337b6 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -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, } diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js new file mode 100644 index 0000000000..3d7253e624 --- /dev/null +++ b/packages/auth/src/redis/index.js @@ -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} 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} 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 diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js new file mode 100644 index 0000000000..90f82d041a --- /dev/null +++ b/packages/auth/src/redis/utils.js @@ -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] + } +} diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index c3066ebdc1..0dbdaadf8d 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -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" diff --git a/packages/cli/src/hosting/makeEnv.js b/packages/cli/src/hosting/makeEnv.js index 318a72def1..a4fbce6ee0 100644 --- a/packages/cli/src/hosting/makeEnv.js +++ b/packages/cli/src/hosting/makeEnv.js @@ -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 diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index dc15bc8a9a..061f38a985 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -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, diff --git a/packages/worker/src/api/controllers/admin/email.js b/packages/worker/src/api/controllers/admin/email.js index 9f4060d20f..0a29468133 100644 --- a/packages/worker/src/api/controllers/admin/email.js +++ b/packages/worker/src/api/controllers/admin/email.js @@ -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}.`, diff --git a/packages/worker/src/api/controllers/auth.js b/packages/worker/src/api/controllers/auth.js index bcda523a93..05895c8358 100644 --- a/packages/worker/src/api/controllers/auth.js +++ b/packages/worker/src/api/controllers/auth.js @@ -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("/") } diff --git a/packages/worker/src/api/routes/auth.js b/packages/worker/src/api/routes/auth.js index 72fddec399..e914f334d7 100644 --- a/packages/worker/src/api/routes/auth.js +++ b/packages/worker/src/api/routes/auth.js @@ -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 diff --git a/packages/worker/src/environment.js b/packages/worker/src/environment.js index 79c9cd1109..0adfe2afae 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.js @@ -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, diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index b5e64a2793..f1059c081e 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -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() }