Merge branch 'next' of github.com:Budibase/budibase into app-list
This commit is contained in:
commit
1ed08a8a8b
|
@ -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,8 +104,7 @@ services:
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: always
|
restart: always
|
||||||
image: redis
|
image: redis
|
||||||
ports:
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
- "${REDIS_PORT}:6379"
|
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,6 @@ static_resources:
|
||||||
cluster: couchdb-service
|
cluster: couchdb-service
|
||||||
prefix_rewrite: "/"
|
prefix_rewrite: "/"
|
||||||
|
|
||||||
- match: { prefix: "/cache/" }
|
|
||||||
route:
|
|
||||||
cluster: redis-service
|
|
||||||
prefix_rewrite: "/"
|
|
||||||
|
|
||||||
- match: { prefix: "/api/admin/" }
|
- match: { prefix: "/api/admin/" }
|
||||||
route:
|
route:
|
||||||
cluster: worker-dev
|
cluster: worker-dev
|
||||||
|
@ -85,20 +80,6 @@ static_resources:
|
||||||
address: couchdb-service
|
address: couchdb-service
|
||||||
port_value: 5984
|
port_value: 5984
|
||||||
|
|
||||||
- name: redis-service
|
|
||||||
connect_timeout: 0.25s
|
|
||||||
type: strict_dns
|
|
||||||
lb_policy: round_robin
|
|
||||||
load_assignment:
|
|
||||||
cluster_name: redis-service
|
|
||||||
endpoints:
|
|
||||||
- lb_endpoints:
|
|
||||||
- endpoint:
|
|
||||||
address:
|
|
||||||
socket_address:
|
|
||||||
address: redis-service
|
|
||||||
port_value: 6379
|
|
||||||
|
|
||||||
- name: server-dev
|
- name: server-dev
|
||||||
connect_timeout: 0.25s
|
connect_timeout: 0.25s
|
||||||
type: strict_dns
|
type: strict_dns
|
||||||
|
|
|
@ -41,11 +41,6 @@ static_resources:
|
||||||
cluster: worker-service
|
cluster: worker-service
|
||||||
prefix_rewrite: "/"
|
prefix_rewrite: "/"
|
||||||
|
|
||||||
- match: { prefix: "/cache/" }
|
|
||||||
route:
|
|
||||||
cluster: redis-service
|
|
||||||
prefix_rewrite: "/"
|
|
||||||
|
|
||||||
- match: { prefix: "/db/" }
|
- match: { prefix: "/db/" }
|
||||||
route:
|
route:
|
||||||
cluster: couchdb-service
|
cluster: couchdb-service
|
||||||
|
@ -117,18 +112,3 @@ static_resources:
|
||||||
address: couchdb-service
|
address: couchdb-service
|
||||||
port_value: 5984
|
port_value: 5984
|
||||||
|
|
||||||
- name: redis-service
|
|
||||||
connect_timeout: 0.25s
|
|
||||||
type: strict_dns
|
|
||||||
lb_policy: round_robin
|
|
||||||
load_assignment:
|
|
||||||
cluster_name: redis-service
|
|
||||||
endpoints:
|
|
||||||
- lb_endpoints:
|
|
||||||
- endpoint:
|
|
||||||
address:
|
|
||||||
socket_address:
|
|
||||||
address: redis-service
|
|
||||||
port_value: 6379
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "0.0.1",
|
"version": "0.18.6",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"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",
|
||||||
|
@ -14,5 +15,8 @@
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ioredis-mock": "^5.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ const getConfigParams = ({ type, group, user }, otherProps = {}) => {
|
||||||
* @param {Object} scopes - the type, group and userID scopes of the configuration.
|
* @param {Object} scopes - the type, group and userID scopes of the configuration.
|
||||||
* @returns The most granular configuration document based on the scope.
|
* @returns The most granular configuration document based on the scope.
|
||||||
*/
|
*/
|
||||||
const determineScopedConfig = async function (db, { type, user, group }) {
|
const getScopedFullConfig = async function (db, { type, user, group }) {
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getConfigParams(
|
getConfigParams(
|
||||||
{ type, user, group },
|
{ type, user, group },
|
||||||
|
@ -157,9 +157,15 @@ const determineScopedConfig = async function (db, { type, user, group }) {
|
||||||
(a, b) => determineScore(a) - determineScore(b)
|
(a, b) => determineScore(a) - determineScore(b)
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
return scopedConfig.doc
|
return scopedConfig && scopedConfig.doc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getScopedConfig(db, params) {
|
||||||
|
const configDoc = await getScopedFullConfig(db, params)
|
||||||
|
return configDoc && configDoc.config ? configDoc.config : configDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getScopedConfig = getScopedConfig
|
||||||
exports.generateConfigID = generateConfigID
|
exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.determineScopedConfig = determineScopedConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
function isTest() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV === "jest" ||
|
||||||
|
process.env.NODE_ENV === "cypress" ||
|
||||||
|
process.env.JEST_WORKER_ID != null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
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,
|
||||||
|
isTest,
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,10 @@ module.exports = {
|
||||||
setDB(pouch)
|
setDB(pouch)
|
||||||
},
|
},
|
||||||
db: require("./db/utils"),
|
db: require("./db/utils"),
|
||||||
|
redis: {
|
||||||
|
Client: require("./redis"),
|
||||||
|
utils: require("./redis/utils"),
|
||||||
|
},
|
||||||
utils: {
|
utils: {
|
||||||
...require("./utils"),
|
...require("./utils"),
|
||||||
...require("./hashing"),
|
...require("./hashing"),
|
||||||
|
|
|
@ -3,11 +3,35 @@ const database = require("../db")
|
||||||
const { getCookie, clearCookie } = require("../utils")
|
const { getCookie, clearCookie } = require("../utils")
|
||||||
const { StaticDatabases } = require("../db/utils")
|
const { StaticDatabases } = require("../db/utils")
|
||||||
|
|
||||||
module.exports = (noAuthPatterns = []) => {
|
const PARAM_REGEX = /\/:(.*?)\//g
|
||||||
const regex = new RegExp(noAuthPatterns.join("|"))
|
|
||||||
|
function buildNoAuthRegex(patterns) {
|
||||||
|
return patterns.map(pattern => {
|
||||||
|
const isObj = typeof pattern === "object" && pattern.route
|
||||||
|
const method = isObj ? pattern.method : "GET"
|
||||||
|
let route = isObj ? pattern.route : pattern
|
||||||
|
|
||||||
|
const matches = route.match(PARAM_REGEX)
|
||||||
|
if (matches) {
|
||||||
|
for (let match of matches) {
|
||||||
|
route = route.replace(match, "/.*/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { regex: new RegExp(route), method }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (noAuthPatterns = [], opts) => {
|
||||||
|
const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : []
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
// the path is not authenticated
|
// the path is not authenticated
|
||||||
if (regex.test(ctx.request.url)) {
|
const found = noAuthOptions.find(({ regex, method }) => {
|
||||||
|
return (
|
||||||
|
regex.test(ctx.request.url) &&
|
||||||
|
ctx.request.method.toLowerCase() === method.toLowerCase()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (found != null) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -30,10 +54,14 @@ module.exports = (noAuthPatterns = []) => {
|
||||||
if (ctx.isAuthenticated !== true) {
|
if (ctx.isAuthenticated !== true) {
|
||||||
ctx.isAuthenticated = false
|
ctx.isAuthenticated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// allow configuring for public access
|
||||||
|
if (opts && opts.publicAllowed) {
|
||||||
|
ctx.isAuthenticated = false
|
||||||
|
} else {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
// ioredis mock is all in memory
|
||||||
|
const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis")
|
||||||
|
const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils")
|
||||||
|
|
||||||
|
const CLUSTERED = false
|
||||||
|
|
||||||
|
// for testing just generate the client once
|
||||||
|
let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
||||||
|
// testing uses a single in memory client
|
||||||
|
if (env.isTest()) {
|
||||||
|
return resolve(CLIENT)
|
||||||
|
}
|
||||||
|
// if a connection existed, close it and re-create it
|
||||||
|
if (CLIENT) {
|
||||||
|
CLIENT.disconnect()
|
||||||
|
CLIENT = null
|
||||||
|
}
|
||||||
|
const { opts, host, port } = getRedisOptions(CLUSTERED)
|
||||||
|
if (CLUSTERED) {
|
||||||
|
CLIENT = new Redis.Cluster([{ host, port }], opts)
|
||||||
|
} else {
|
||||||
|
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 finish() {
|
||||||
|
this._client.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// if its not an object just return the response
|
||||||
|
try {
|
||||||
|
return JSON.parse(response)
|
||||||
|
} catch (err) {
|
||||||
|
return 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,46 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const SLOT_REFRESH_MS = 2000
|
||||||
|
const CONNECT_TIMEOUT_MS = 10000
|
||||||
|
const SEPARATOR = "-"
|
||||||
|
const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL
|
||||||
|
const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD
|
||||||
|
|
||||||
|
exports.Databases = {
|
||||||
|
PW_RESETS: "pwReset",
|
||||||
|
INVITATIONS: "invitation",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getRedisOptions = (clustered = false) => {
|
||||||
|
const [host, port] = REDIS_URL.split(":")
|
||||||
|
const opts = {
|
||||||
|
connectTimeout: CONNECT_TIMEOUT_MS,
|
||||||
|
}
|
||||||
|
if (clustered) {
|
||||||
|
opts.redisOptions = {}
|
||||||
|
opts.redisOptions.tls = {}
|
||||||
|
opts.redisOptions.password = REDIS_PASSWORD
|
||||||
|
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
||||||
|
opts.dnsLookup = (address, callback) => callback(null, address)
|
||||||
|
} else {
|
||||||
|
opts.host = host
|
||||||
|
opts.port = port
|
||||||
|
opts.password = REDIS_PASSWORD
|
||||||
|
}
|
||||||
|
return { opts, host, port }
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,6 +105,12 @@ exports.isClient = ctx => {
|
||||||
return ctx.headers["x-budibase-type"] === "client"
|
return ctx.headers["x-budibase-type"] === "client"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an email address this will use a view to search through
|
||||||
|
* all the users to find one with this email address.
|
||||||
|
* @param {string} email the email to lookup the user by.
|
||||||
|
* @return {Promise<object|null>}
|
||||||
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
exports.getGlobalUserByEmail = async email => {
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
const db = getDB(StaticDatabases.GLOBAL.name)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -46,6 +46,11 @@ aws4@^1.8.0:
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||||
|
|
||||||
|
balanced-match@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
base64url@3.x.x:
|
base64url@3.x.x:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
||||||
|
@ -63,6 +68,14 @@ bcryptjs@^2.4.3:
|
||||||
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
||||||
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
|
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
|
||||||
|
|
||||||
|
brace-expansion@^1.1.7:
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
|
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^1.0.0"
|
||||||
|
concat-map "0.0.1"
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||||
|
@ -73,6 +86,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"
|
||||||
|
@ -80,6 +98,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
concat-map@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
|
@ -92,11 +115,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"
|
||||||
|
@ -137,6 +172,20 @@ fast-json-stable-stringify@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||||
|
|
||||||
|
fengari-interop@^0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.2.tgz#f7731dcdd2ff4449073fb7ac3c451a8841ce1e87"
|
||||||
|
integrity sha512-8iTvaByZVoi+lQJhHH9vC+c/Yaok9CwOqNQZN6JrVpjmWwW4dDkeblBXhnHC+BoI6eF4Cy5NKW3z6ICEjvgywQ==
|
||||||
|
|
||||||
|
fengari@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/fengari/-/fengari-0.1.4.tgz#72416693cd9e43bd7d809d7829ddc0578b78b0bb"
|
||||||
|
integrity sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==
|
||||||
|
dependencies:
|
||||||
|
readline-sync "^1.4.9"
|
||||||
|
sprintf-js "^1.1.1"
|
||||||
|
tmp "^0.0.33"
|
||||||
|
|
||||||
forever-agent@~0.6.1:
|
forever-agent@~0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
|
@ -216,6 +265,33 @@ http-signature@~1.2.0:
|
||||||
jsprim "^1.2.2"
|
jsprim "^1.2.2"
|
||||||
sshpk "^1.7.0"
|
sshpk "^1.7.0"
|
||||||
|
|
||||||
|
ioredis-mock@^5.5.5:
|
||||||
|
version "5.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.5.5.tgz#dec9fedd238c6ab9f56c026fc366533144f8a256"
|
||||||
|
integrity sha512-7SxCAwNtDLC8IFDptqIhOC7ajp3fciVtCrXOEOkpyjPboAGRQkJbnpNPy1NYORoWi+0/iOtUPUQckSKtSQj4DA==
|
||||||
|
dependencies:
|
||||||
|
fengari "^0.1.4"
|
||||||
|
fengari-interop "^0.1.2"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
standard-as-callback "^2.1.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 +372,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"
|
||||||
|
@ -336,7 +422,7 @@ lodash.once@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
||||||
|
|
||||||
lodash@^4.14.0:
|
lodash@^4.14.0, lodash@^4.17.21:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -358,6 +444,18 @@ 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==
|
||||||
|
|
||||||
|
minimatch@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
|
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||||
|
dependencies:
|
||||||
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
|
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 +476,16 @@ 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=
|
||||||
|
|
||||||
|
os-tmpdir@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
|
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||||
|
|
||||||
|
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 +589,28 @@ 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==
|
||||||
|
|
||||||
|
readline-sync@^1.4.9:
|
||||||
|
version "1.4.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
|
||||||
|
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
|
||||||
|
|
||||||
|
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"
|
||||||
|
@ -522,6 +652,11 @@ semver@^5.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
|
sprintf-js@^1.1.1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
|
||||||
|
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
||||||
|
|
||||||
sshpk@^1.7.0:
|
sshpk@^1.7.0:
|
||||||
version "1.16.1"
|
version "1.16.1"
|
||||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||||
|
@ -537,11 +672,23 @@ 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"
|
||||||
integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
|
integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
|
||||||
|
|
||||||
|
tmp@^0.0.33:
|
||||||
|
version "0.0.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||||
|
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||||
|
dependencies:
|
||||||
|
os-tmpdir "~1.0.2"
|
||||||
|
|
||||||
tough-cookie@~2.5.0:
|
tough-cookie@~2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import { isActive, url, goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
Checkbox,
|
||||||
|
Body,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
Avatar,
|
||||||
|
Search,
|
||||||
|
Layout,
|
||||||
|
ProgressCircle,
|
||||||
|
SideNavigation as Navigation,
|
||||||
|
SideNavigationItem as Item,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import { organisation, admin } from "stores/portal"
|
||||||
|
|
||||||
|
const MESSAGES = {
|
||||||
|
apps: "Create your first app",
|
||||||
|
smtp: "Set up email",
|
||||||
|
adminUser: "Create your first user",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionMenu>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<ProgressCircle size="S" value={$admin.onboardingProgress} />
|
||||||
|
</div>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<header class="item">
|
||||||
|
<Heading size="XXS">Get Started Checklist</Heading>
|
||||||
|
<ProgressCircle size="S" value={$admin.onboardingProgress} />
|
||||||
|
</header>
|
||||||
|
</MenuItem>
|
||||||
|
{#each Object.keys($admin.checklist) as checklistItem, idx}
|
||||||
|
<MenuItem>
|
||||||
|
<div class="item">
|
||||||
|
<span>{idx + 1}. {MESSAGES[checklistItem]}</span>
|
||||||
|
<Checkbox value={!!$admin.checklist[checklistItem]} />
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
{/each}
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.item {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 200px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
SideNavigation as Navigation,
|
||||||
|
SideNavigationItem as Item,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
import LoginForm from "components/login/LoginForm.svelte"
|
||||||
|
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
|
||||||
|
import LogoutButton from "components/start/LogoutButton.svelte"
|
||||||
|
import Logo from "/assets/budibase-logo.svg"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
let checklist
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await admin.init()
|
||||||
|
if (!$admin?.checklist?.adminUser) {
|
||||||
|
$goto("./admin")
|
||||||
|
} else {
|
||||||
|
$goto("./portal")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $admin.checklist}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
Body,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
let adminUser = {}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
// Save the admin user
|
||||||
|
const response = await api.post(`/api/admin/users/init`, adminUser)
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
notifications.success(`Admin user created.`)
|
||||||
|
$goto("../portal")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to create admin user.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<Heading size="M">Create an admin user</Heading>
|
||||||
|
<Body size="S">The admin user has access to everything in budibase.</Body>
|
||||||
|
</header>
|
||||||
|
<div class="config-form">
|
||||||
|
<Layout gap="S">
|
||||||
|
<Input label="email" bind:value={adminUser.email} />
|
||||||
|
<Input
|
||||||
|
label="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={adminUser.password}
|
||||||
|
/>
|
||||||
|
<Button cta on:click={save}>Create super admin user</Button>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,7 +23,6 @@
|
||||||
<div class="nav-top">
|
<div class="nav-top">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Item href="/builder/" icon="Apps" selected>Apps</Item>
|
<Item href="/builder/" icon="Apps" selected>Apps</Item>
|
||||||
<Item href="/builder/oauth/" icon="OAuth" selected>OAuth</Item>
|
|
||||||
<Item external href="https://portal.budi.live/" icon="Servers">
|
<Item external href="https://portal.budi.live/" icon="Servers">
|
||||||
Hosting
|
Hosting
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, url } from "@roxi/routify"
|
import { isActive } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Avatar,
|
Avatar,
|
||||||
Search,
|
Search,
|
||||||
Layout,
|
Layout,
|
||||||
ProgressCircle,
|
|
||||||
SideNavigation as Navigation,
|
SideNavigation as Navigation,
|
||||||
SideNavigationItem as Item,
|
SideNavigationItem as Item,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||||
import { organisation, apps } from "stores/portal"
|
import { organisation, apps } from "stores/portal"
|
||||||
|
|
||||||
organisation.init()
|
organisation.init()
|
||||||
apps.load()
|
apps.load()
|
||||||
|
|
||||||
console.log("loading")
|
let orgName
|
||||||
|
let orgLogo
|
||||||
let onBoardingProgress, user
|
let user
|
||||||
|
|
||||||
async function getInfo() {
|
async function getInfo() {
|
||||||
onBoardingProgress = 20
|
// fetch orgInfo
|
||||||
|
orgName = "ACME Inc."
|
||||||
|
orgLogo = "https://via.placeholder.com/150"
|
||||||
user = { name: "John Doe" }
|
user = { name: "John Doe" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,15 +53,13 @@
|
||||||
<span>{$organisation?.company || "Budibase"}</span>
|
<span>{$organisation?.company || "Budibase"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding">
|
<div class="onboarding">
|
||||||
<ProgressCircle size="S" value={onBoardingProgress} />
|
<ConfigChecklist />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
{#each menu as { title, href, heading }}
|
{#each menu as { title, href, heading }}
|
||||||
<Item selected={$isActive(href)} href={$url(href)} {heading}>
|
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
|
||||||
{title}
|
|
||||||
</Item>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,36 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
const INITIAL_ADMIN_STATE = {
|
export function createAdminStore() {
|
||||||
oauth: [],
|
const { subscribe, set } = writable({})
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
const response = await api.get("/api/admin/configs/checklist")
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
const onboardingSteps = Object.keys(json)
|
||||||
|
|
||||||
|
const stepsComplete = onboardingSteps.reduce(
|
||||||
|
(score, step) => score + Number(!!json[step]),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
set({
|
||||||
|
checklist: json,
|
||||||
|
onboardingProgress: (stepsComplete / onboardingSteps.length) * 100,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
checklist: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
init,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const admin = writable({ ...INITIAL_ADMIN_STATE })
|
export const admin = createAdminStore()
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.0.1",
|
"@budibase/auth": "^0.18.6",
|
||||||
"@budibase/client": "^0.8.16",
|
"@budibase/client": "^0.8.16",
|
||||||
"@budibase/string-templates": "^0.8.16",
|
"@budibase/string-templates": "^0.8.16",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -37,9 +37,10 @@ async function init() {
|
||||||
PORT: 4001,
|
PORT: 4001,
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
REDIS_URL: "http://localhost:10000/cache/",
|
REDIS_URL: "localhost:6379",
|
||||||
WORKER_URL: "http://localhost:4002",
|
WORKER_URL: "http://localhost:4002",
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
COUCH_DB_PASSWORD: "budibase",
|
||||||
|
|
|
@ -5,20 +5,16 @@ const compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
const { mainRoutes, staticRoutes } = require("./routes")
|
const { mainRoutes, staticRoutes } = require("./routes")
|
||||||
const pkg = require("../../package.json")
|
const pkg = require("../../package.json")
|
||||||
const bullboard = require("bull-board")
|
|
||||||
const expressApp = require("express")()
|
|
||||||
|
|
||||||
expressApp.use("/bulladmin", bullboard.router)
|
|
||||||
|
|
||||||
const router = new Router()
|
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const NO_AUTH_ENDPOINTS = [
|
if (!env.isTest()) {
|
||||||
"/health",
|
const bullboard = require("bull-board")
|
||||||
"/version",
|
const expressApp = require("express")()
|
||||||
"webhooks/trigger",
|
|
||||||
"webhooks/schema",
|
expressApp.use("/bulladmin", bullboard.router)
|
||||||
]
|
}
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
|
@ -42,7 +38,11 @@ router
|
||||||
})
|
})
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use("/version", ctx => (ctx.body = pkg.version))
|
.use("/version", ctx => (ctx.body = pkg.version))
|
||||||
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
.use(
|
||||||
|
buildAuthMiddleware(null, {
|
||||||
|
publicAllowed: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
.use(currentApp)
|
.use(currentApp)
|
||||||
|
|
||||||
// error handling middleware
|
// error handling middleware
|
||||||
|
|
|
@ -402,14 +402,16 @@ describe("/rows", () => {
|
||||||
name: "test",
|
name: "test",
|
||||||
description: "test",
|
description: "test",
|
||||||
attachment: [{
|
attachment: [{
|
||||||
url: "/test/thing",
|
key: `/assets/${config.getAppId()}/attachment/test/thing.csv`,
|
||||||
}],
|
}],
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
})
|
})
|
||||||
// the environment needs configured for this
|
// the environment needs configured for this
|
||||||
await setup.switchToSelfHosted(async () => {
|
await setup.switchToSelfHosted(async () => {
|
||||||
const enriched = await outputProcessing(config.getAppId(), table, [row])
|
const enriched = await outputProcessing(config.getAppId(), table, [row])
|
||||||
expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`)
|
expect(enriched[0].attachment[0].url).toBe(
|
||||||
|
`/prod-budi-app-assets/assets/${config.getAppId()}/attachment/test/thing.csv`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const emitter = require("../events/index")
|
const emitter = require("../events/index")
|
||||||
const Queue = require("bull")
|
const env = require("../environment")
|
||||||
|
const Queue = env.isTest()
|
||||||
|
? require("../utilities/queue/inMemoryQueue")
|
||||||
|
: require("bull")
|
||||||
const { setQueues, BullAdapter } = require("bull-board")
|
const { setQueues, BullAdapter } = require("bull-board")
|
||||||
const { getAutomationParams } = require("../db/utils")
|
const { getAutomationParams } = require("../db/utils")
|
||||||
const { coerce } = require("../utilities/rowProcessor")
|
const { coerce } = require("../utilities/rowProcessor")
|
||||||
|
const { utils } = require("@budibase/auth").redis
|
||||||
|
|
||||||
let automationQueue = new Queue("automationQueue")
|
const { opts } = utils.getRedisOptions()
|
||||||
|
let automationQueue = new Queue("automationQueue", { redis: opts })
|
||||||
|
|
||||||
// Set up queues for bull board admin
|
// Set up queues for bull board admin
|
||||||
setQueues([new BullAdapter(automationQueue)])
|
setQueues([new BullAdapter(automationQueue)])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,12 +14,13 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"run:docker": "node src/index.js",
|
"run:docker": "node src/index.js",
|
||||||
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
||||||
"dev:builder": "npm run dev:stack:init && nodemon src/index.js"
|
"dev:builder": "npm run dev:stack:init && nodemon src/index.js",
|
||||||
|
"test": "jest --runInBand"
|
||||||
},
|
},
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "0.0.1",
|
"@budibase/auth": "^0.18.6",
|
||||||
"@budibase/string-templates": "^0.8.16",
|
"@budibase/string-templates": "^0.8.16",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"aws-sdk": "^2.811.0",
|
"aws-sdk": "^2.811.0",
|
||||||
|
@ -50,5 +51,11 @@
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pouchdb-adapter-memory": "^7.2.2",
|
"pouchdb-adapter-memory": "^7.2.2",
|
||||||
"supertest": "^6.1.3"
|
"supertest": "^6.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"setupFiles": [
|
||||||
|
"./scripts/jestSetup.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ async function init() {
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
COUCH_DB_PASSWORD: "budibase",
|
||||||
|
REDIS_URL: "localhost:6379",
|
||||||
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
const env = require("../src/environment")
|
||||||
|
|
||||||
|
env._set("NODE_ENV", "jest")
|
||||||
|
env._set("JWT_SECRET", "test-jwtsecret")
|
||||||
|
env._set("LOG_LEVEL", "silent")
|
|
@ -0,0 +1,125 @@
|
||||||
|
const authPkg = require("@budibase/auth")
|
||||||
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
|
const { Configs, EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
|
||||||
|
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
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
authInternal(ctx, user, err)
|
||||||
|
|
||||||
|
delete user.token
|
||||||
|
|
||||||
|
ctx.body = { user }
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the user password, used as part of a forgotten password flow.
|
||||||
|
*/
|
||||||
|
exports.reset = async ctx => {
|
||||||
|
const { email } = ctx.request.body
|
||||||
|
const configured = await isEmailConfigured()
|
||||||
|
if (!configured) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
"Please contact your platform administrator, SMTP is not configured."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const user = await getGlobalUserByEmail(email)
|
||||||
|
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: "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
|
||||||
|
try {
|
||||||
|
const userId = await checkResetPasswordCode(resetCode)
|
||||||
|
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.",
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(400, "Cannot reset password.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logout = async ctx => {
|
||||||
|
clearCookie(ctx, Cookies.Auth)
|
||||||
|
ctx.body = { message: "User logged out." }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial call that google authentication makes to take you to the google login screen.
|
||||||
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
|
*/
|
||||||
|
exports.googlePreAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const config = await authPkg.db.getScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.googleAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
const config = await authPkg.db.getScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, user) => {
|
||||||
|
authInternal(ctx, user, err)
|
||||||
|
|
||||||
|
ctx.redirect("/")
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
|
}
|
|
@ -3,10 +3,15 @@ const {
|
||||||
generateConfigID,
|
generateConfigID,
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
getConfigParams,
|
getConfigParams,
|
||||||
determineScopedConfig,
|
getGlobalUserParams,
|
||||||
|
getScopedFullConfig,
|
||||||
} = require("@budibase/auth").db
|
} = require("@budibase/auth").db
|
||||||
|
const fetch = require("node-fetch")
|
||||||
const { Configs } = require("../../../constants")
|
const { Configs } = require("../../../constants")
|
||||||
const email = require("../../../utilities/email")
|
const email = require("../../../utilities/email")
|
||||||
|
const env = require("../../../environment")
|
||||||
|
|
||||||
|
const APP_PREFIX = "app_"
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
@ -45,9 +50,12 @@ exports.save = async function (ctx) {
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getConfigParams(undefined, {
|
getConfigParams(
|
||||||
|
{ type: ctx.params.type },
|
||||||
|
{
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
@ -58,11 +66,10 @@ exports.fetch = async function (ctx) {
|
||||||
*/
|
*/
|
||||||
exports.find = async function (ctx) {
|
exports.find = async function (ctx) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const userId = ctx.params.user && ctx.params.user._id
|
|
||||||
|
|
||||||
const { group } = ctx.query
|
const { userId, groupId } = ctx.query
|
||||||
if (group) {
|
if (groupId && userId) {
|
||||||
const group = await db.get(group)
|
const group = await db.get(groupId)
|
||||||
const userInGroup = group.users.some(groupUser => groupUser === userId)
|
const userInGroup = group.users.some(groupUser => groupUser === userId)
|
||||||
if (!ctx.user.admin && !userInGroup) {
|
if (!ctx.user.admin && !userInGroup) {
|
||||||
ctx.throw(400, `User is not in specified group: ${group}.`)
|
ctx.throw(400, `User is not in specified group: ${group}.`)
|
||||||
|
@ -71,10 +78,10 @@ exports.find = async function (ctx) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the config with the most granular scope based on context
|
// Find the config with the most granular scope based on context
|
||||||
const scopedConfig = await determineScopedConfig(db, {
|
const scopedConfig = await getScopedFullConfig(db, {
|
||||||
type: ctx.params.type,
|
type: ctx.params.type,
|
||||||
user: userId,
|
user: userId,
|
||||||
group,
|
group: groupId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (scopedConfig) {
|
if (scopedConfig) {
|
||||||
|
@ -98,3 +105,41 @@ exports.destroy = async function (ctx) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.configChecklist = async function (ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Watch get started video
|
||||||
|
|
||||||
|
// Apps exist
|
||||||
|
let allDbs
|
||||||
|
if (env.COUCH_DB_URL) {
|
||||||
|
allDbs = await (await fetch(`${env.COUCH_DB_URL}/_all_dbs`)).json()
|
||||||
|
} else {
|
||||||
|
allDbs = await CouchDB.allDbs()
|
||||||
|
}
|
||||||
|
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
|
||||||
|
|
||||||
|
// They have set up SMTP
|
||||||
|
const smtpConfig = await getScopedFullConfig(db, {
|
||||||
|
type: Configs.SMTP,
|
||||||
|
})
|
||||||
|
|
||||||
|
// They have set up an admin user
|
||||||
|
const users = await db.allDocs(
|
||||||
|
getGlobalUserParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const adminUser = users.rows.some(row => row.doc.admin)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
apps: appDbNames.length,
|
||||||
|
smtp: !!smtpConfig,
|
||||||
|
adminUser,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,82 +1,17 @@
|
||||||
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const { StaticDatabases, determineScopedConfig } = require("@budibase/auth").db
|
const authPkg = require("@budibase/auth")
|
||||||
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 GLOBAL_DB = authPkg.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)
|
let user
|
||||||
const params = {}
|
|
||||||
if (groupId) {
|
|
||||||
params.group = groupId
|
|
||||||
}
|
|
||||||
params.type = Configs.SMTP
|
|
||||||
let user = {}
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
user = db.get(userId)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
user = await db.get(userId)
|
||||||
}
|
}
|
||||||
const { config } = await determineScopedConfig(db, params)
|
const response = await sendEmail(email, purpose, { groupId, user })
|
||||||
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}.`,
|
||||||
|
|
|
@ -5,10 +5,10 @@ const {
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
} = require("@budibase/auth").db
|
} = require("@budibase/auth").db
|
||||||
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||||
const { UserStatus } = require("../../../constants")
|
const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
const { checkInviteCode } = require("../../../utilities/redis")
|
||||||
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
|
|
||||||
const FIRST_USER_EMAIL = "test@test.com"
|
|
||||||
const FIRST_USER_PASSWORD = "test"
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
|
@ -42,7 +42,7 @@ exports.save = async ctx => {
|
||||||
user.status = UserStatus.ACTIVE
|
user.status = UserStatus.ACTIVE
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await db.post({
|
const response = await db.put({
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
...user,
|
...user,
|
||||||
})
|
})
|
||||||
|
@ -60,14 +60,29 @@ exports.save = async ctx => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.firstUser = async ctx => {
|
exports.adminUser = async ctx => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getGlobalUserParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.rows.some(row => row.doc.admin)) {
|
||||||
|
ctx.throw(403, "You cannot initialise once an admin user has been created.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, password } = ctx.request.body
|
||||||
ctx.request.body = {
|
ctx.request.body = {
|
||||||
email: FIRST_USER_EMAIL,
|
email: email,
|
||||||
password: FIRST_USER_PASSWORD,
|
password: password,
|
||||||
roles: {},
|
roles: {},
|
||||||
builder: {
|
builder: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await exports.save(ctx)
|
await exports.save(ctx)
|
||||||
}
|
}
|
||||||
|
@ -114,3 +129,29 @@ exports.find = async ctx => {
|
||||||
}
|
}
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.invite = async ctx => {
|
||||||
|
const { email } = ctx.request.body
|
||||||
|
const existing = await getGlobalUserByEmail(email)
|
||||||
|
if (existing) {
|
||||||
|
ctx.throw(400, "Email address already in use.")
|
||||||
|
}
|
||||||
|
await sendEmail(email, EmailTemplatePurpose.INVITATION)
|
||||||
|
ctx.body = {
|
||||||
|
message: "Invitation has been sent.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.inviteAccept = async ctx => {
|
||||||
|
const { inviteCode } = ctx.request.body
|
||||||
|
try {
|
||||||
|
const email = await checkInviteCode(inviteCode)
|
||||||
|
// redirect the request
|
||||||
|
delete ctx.request.body.inviteCode
|
||||||
|
ctx.request.body.email = email
|
||||||
|
// this will flesh out the body response
|
||||||
|
await exports.save(ctx)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
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 { Cookies } = authPkg.constants
|
|
||||||
const { passport } = authPkg.auth
|
|
||||||
|
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
delete user.token
|
|
||||||
|
|
||||||
ctx.body = { user }
|
|
||||||
})(ctx, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.logout = async ctx => {
|
|
||||||
clearCookie(ctx, Cookies.Auth)
|
|
||||||
ctx.body = { message: "User logged out" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initial call that google authentication makes to take you to the google login screen.
|
|
||||||
* On a successful login, you will be redirected to the googleAuth callback route.
|
|
||||||
*/
|
|
||||||
exports.googlePreAuth = async (ctx, next) => {
|
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
|
||||||
const config = await authPkg.db.determineScopedConfig(db, {
|
|
||||||
type: Configs.GOOGLE,
|
|
||||||
group: ctx.query.group,
|
|
||||||
})
|
|
||||||
const strategy = await google.strategyFactory(config)
|
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
|
||||||
scope: ["profile", "email"],
|
|
||||||
})(ctx, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.googleAuth = async (ctx, next) => {
|
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
|
||||||
|
|
||||||
const config = await authPkg.db.determineScopedConfig(db, {
|
|
||||||
type: Configs.GOOGLE,
|
|
||||||
group: ctx.query.group,
|
|
||||||
})
|
|
||||||
const strategy = await google.strategyFactory(config)
|
|
||||||
|
|
||||||
return passport.authenticate(
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.redirect("/")
|
|
||||||
}
|
|
||||||
)(ctx, next)
|
|
||||||
}
|
|
|
@ -4,15 +4,38 @@ const zlib = require("zlib")
|
||||||
const { routes } = require("./routes")
|
const { routes } = require("./routes")
|
||||||
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||||
|
|
||||||
const NO_AUTH_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
"/api/admin/users/first",
|
{
|
||||||
"/api/admin/auth",
|
route: "/api/admin/users/init",
|
||||||
"/api/admin/auth/google",
|
method: "POST",
|
||||||
"/api/admin/auth/google/callback",
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/users/invite/accept",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/google",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/google/callback",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/auth/reset",
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/configs/checklist",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
compress({
|
compress({
|
||||||
|
@ -27,7 +50,7 @@ router
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (!ctx.isAuthenticated) {
|
if (!ctx.isAuthenticated) {
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const authController = require("../../controllers/admin/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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -57,14 +57,27 @@ function buildConfigSaveValidation() {
|
||||||
{ is: Configs.GOOGLE, then: googleValidation() }
|
{ is: Configs.GOOGLE, then: googleValidation() }
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
}),
|
}).required(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildConfigGetValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.params(Joi.object({
|
||||||
|
type: Joi.string().valid(...Object.values(Configs)).required()
|
||||||
|
}).unknown(true).required())
|
||||||
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
||||||
.delete("/api/admin/configs/:id", controller.destroy)
|
.delete("/api/admin/configs/:id", controller.destroy)
|
||||||
.get("/api/admin/configs", controller.fetch)
|
.get("/api/admin/configs", controller.fetch)
|
||||||
.get("/api/admin/configs/:type", controller.find)
|
.get("/api/admin/configs/checklist", controller.configChecklist)
|
||||||
|
.get(
|
||||||
|
"/api/admin/configs/all/:type",
|
||||||
|
buildConfigGetValidation(),
|
||||||
|
controller.fetch
|
||||||
|
)
|
||||||
|
.get("/api/admin/configs/:type", buildConfigGetValidation(), controller.find)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -21,14 +21,35 @@ function buildUserSaveValidation() {
|
||||||
.pattern(/.*/, Joi.string())
|
.pattern(/.*/, Joi.string())
|
||||||
.required()
|
.required()
|
||||||
.unknown(true)
|
.unknown(true)
|
||||||
}).required().unknown(true).optional())
|
}).required().unknown(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInviteValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
email: Joi.string().required(),
|
||||||
|
}).required())
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInviteAcceptValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
inviteCode: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
||||||
.get("/api/admin/users", controller.fetch)
|
.get("/api/admin/users", controller.fetch)
|
||||||
.post("/api/admin/users/first", controller.firstUser)
|
.post("/api/admin/users/init", controller.adminUser)
|
||||||
.delete("/api/admin/users/:id", controller.destroy)
|
.delete("/api/admin/users/:id", controller.destroy)
|
||||||
.get("/api/admin/users/:id", controller.find)
|
.get("/api/admin/users/:id", controller.find)
|
||||||
|
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
||||||
|
.post(
|
||||||
|
"/api/admin/users/invite/accept",
|
||||||
|
buildInviteAcceptValidation(),
|
||||||
|
controller.inviteAccept
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
const Router = require("@koa/router")
|
|
||||||
const authController = require("../controllers/auth")
|
|
||||||
|
|
||||||
const router = Router()
|
|
||||||
|
|
||||||
router
|
|
||||||
.post("/api/admin/auth", authController.authenticate)
|
|
||||||
.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
|
|
|
@ -3,7 +3,7 @@ const configRoutes = require("./admin/configs")
|
||||||
const groupRoutes = require("./admin/groups")
|
const groupRoutes = require("./admin/groups")
|
||||||
const templateRoutes = require("./admin/templates")
|
const templateRoutes = require("./admin/templates")
|
||||||
const emailRoutes = require("./admin/email")
|
const emailRoutes = require("./admin/email")
|
||||||
const authRoutes = require("./auth")
|
const authRoutes = require("./admin/auth")
|
||||||
const appRoutes = require("./app")
|
const appRoutes = require("./app")
|
||||||
|
|
||||||
exports.routes = [
|
exports.routes = [
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const sendMailMock = setup.emailMock()
|
||||||
|
|
||||||
|
describe("/api/admin/auth", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
let code
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should be able to generate password reset email", async () => {
|
||||||
|
// initially configure settings
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
await config.saveSettingsConfig()
|
||||||
|
await config.createUser("test@test.com")
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/auth/reset`)
|
||||||
|
.send({
|
||||||
|
email: "test@test.com",
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body).toEqual({ message: "Please check your email for a reset link." })
|
||||||
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
|
// after this URL there should be a code
|
||||||
|
const parts = emailCall.html.split("http://localhost:10000/reset?code=")
|
||||||
|
code = parts[1].split("\"")[0]
|
||||||
|
expect(code).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow resetting user password with code", async () => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/auth/reset/update`)
|
||||||
|
.send({
|
||||||
|
password: "newpassword",
|
||||||
|
resetCode: code,
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body).toEqual({ message: "password reset successfully." })
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,38 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
verify: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/api/admin/configs/checklist", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should return the correct checklist status based on the state of the budibase installation", async () => {
|
||||||
|
// initially configure settings
|
||||||
|
await config.saveAdminUser()
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/admin/configs/checklist`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const checklist = res.body
|
||||||
|
|
||||||
|
expect(checklist.apps).toBe(0)
|
||||||
|
expect(checklist.smtp).toBe(true)
|
||||||
|
expect(checklist.adminUser).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,13 +2,8 @@ const setup = require("./utilities")
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
|
||||||
// mock the email system
|
// mock the email system
|
||||||
const sendMailMock = jest.fn()
|
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const nodemailer = require("nodemailer")
|
const sendMailMock = setup.emailMock()
|
||||||
nodemailer.createTransport.mockReturnValue({
|
|
||||||
sendMail: sendMailMock,
|
|
||||||
verify: jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("/api/admin/email", () => {
|
describe("/api/admin/email", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
|
|
|
@ -16,11 +16,13 @@ describe("/api/admin/email", () => {
|
||||||
async function sendRealEmail(purpose) {
|
async function sendRealEmail(purpose) {
|
||||||
await config.saveEtherealSmtpConfig()
|
await config.saveEtherealSmtpConfig()
|
||||||
await config.saveSettingsConfig()
|
await config.saveSettingsConfig()
|
||||||
|
const user = await config.getUser("test@test.com")
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api/admin/email/send`)
|
.post(`/api/admin/email/send`)
|
||||||
.send({
|
.send({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
purpose,
|
purpose,
|
||||||
|
userId: user._id,
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
|
@ -55,6 +57,6 @@ describe("/api/admin/email", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to send a password recovery email", async () => {
|
it("should be able to send a password recovery email", async () => {
|
||||||
const res = await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
|
await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -0,0 +1,52 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const sendMailMock = setup.emailMock()
|
||||||
|
|
||||||
|
describe("/api/admin/users", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
let code
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should be able to generate an invitation", async () => {
|
||||||
|
// initially configure settings
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
await config.saveSettingsConfig()
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/users/invite`)
|
||||||
|
.send({
|
||||||
|
email: "invite@test.com",
|
||||||
|
})
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body).toEqual({ message: "Invitation has been sent." })
|
||||||
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
|
// after this URL there should be a code
|
||||||
|
const parts = emailCall.html.split("http://localhost:10000/invite?code=")
|
||||||
|
code = parts[1].split("\"")[0]
|
||||||
|
expect(code).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to create new user from invite", async () => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/users/invite/accept`)
|
||||||
|
.send({
|
||||||
|
password: "newpassword",
|
||||||
|
inviteCode: code,
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body._id).toBeDefined()
|
||||||
|
const user = await config.getUser("invite@test.com")
|
||||||
|
expect(user).toBeDefined()
|
||||||
|
expect(user._id).toEqual(res.body._id)
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,6 +4,7 @@ const supertest = require("supertest")
|
||||||
const { jwt } = require("@budibase/auth").auth
|
const { jwt } = require("@budibase/auth").auth
|
||||||
const { Cookies } = require("@budibase/auth").constants
|
const { Cookies } = require("@budibase/auth").constants
|
||||||
const { Configs, LOGO_URL } = require("../../../../constants")
|
const { Configs, LOGO_URL } = require("../../../../constants")
|
||||||
|
const { getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
constructor(openServer = true) {
|
constructor(openServer = true) {
|
||||||
|
@ -53,6 +54,12 @@ class TestConfiguration {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async end() {
|
||||||
|
if (this.server) {
|
||||||
|
await this.server.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultHeaders() {
|
defaultHeaders() {
|
||||||
const user = {
|
const user = {
|
||||||
_id: "us_uuid1",
|
_id: "us_uuid1",
|
||||||
|
@ -65,6 +72,25 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUser(email) {
|
||||||
|
return getGlobalUserByEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(email = "test@test.com", password = "test") {
|
||||||
|
const user = await this.getUser(email)
|
||||||
|
if (user) {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.users.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async deleteConfig(type) {
|
async deleteConfig(type) {
|
||||||
try {
|
try {
|
||||||
const cfg = await this._req(
|
const cfg = await this._req(
|
||||||
|
@ -105,6 +131,22 @@ class TestConfiguration {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveOAuthConfig() {
|
||||||
|
await this.deleteConfig(Configs.GOOGLE)
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
config: {
|
||||||
|
callbackURL: "http://somecallbackurl",
|
||||||
|
clientID: "clientId",
|
||||||
|
clientSecret: "clientSecret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.config.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async saveSmtpConfig() {
|
async saveSmtpConfig() {
|
||||||
await this.deleteConfig(Configs.SMTP)
|
await this.deleteConfig(Configs.SMTP)
|
||||||
await this._req(
|
await this._req(
|
||||||
|
@ -141,6 +183,17 @@ class TestConfiguration {
|
||||||
controllers.config.save
|
controllers.config.save
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAdminUser() {
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
email: "testuser@test.com",
|
||||||
|
password: "test@test.com",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.users.adminUser
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TestConfiguration
|
module.exports = TestConfiguration
|
||||||
|
|
|
@ -7,9 +7,9 @@ exports.beforeAll = () => {
|
||||||
request = config.getRequest()
|
request = config.getRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.afterAll = () => {
|
exports.afterAll = async () => {
|
||||||
if (config) {
|
if (config) {
|
||||||
config.end()
|
await config.end()
|
||||||
}
|
}
|
||||||
request = null
|
request = null
|
||||||
config = null
|
config = null
|
||||||
|
@ -28,3 +28,14 @@ exports.getConfig = () => {
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.emailMock = () => {
|
||||||
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
sendMail: sendMailMock,
|
||||||
|
verify: jest.fn(),
|
||||||
|
})
|
||||||
|
return sendMailMock
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,8 @@ const TemplateBindings = {
|
||||||
LOGIN_URL: "loginUrl",
|
LOGIN_URL: "loginUrl",
|
||||||
CURRENT_YEAR: "currentYear",
|
CURRENT_YEAR: "currentYear",
|
||||||
CURRENT_DATE: "currentDate",
|
CURRENT_DATE: "currentDate",
|
||||||
|
RESET_CODE: "resetCode",
|
||||||
|
INVITE_CODE: "inviteCode",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateMetadata = {
|
const TemplateMetadata = {
|
||||||
|
|
|
@ -11,7 +11,7 @@ function isTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let LOADED = false
|
let LOADED = false
|
||||||
if (!LOADED && isDev()) {
|
if (!LOADED && isDev() && !isTest()) {
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
LOADED = true
|
LOADED = true
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -12,10 +12,6 @@ const api = require("./api")
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
throw "Currently this service only supports use in self hosting"
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up top level koa middleware
|
// set up top level koa middleware
|
||||||
app.use(koaBody({ multipart: true }))
|
app.use(koaBody({ multipart: true }))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
const { StaticDatabases, getScopedConfig } = 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")
|
||||||
|
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
||||||
|
|
||||||
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 +31,118 @@ exports.createSMTPTransport = config => {
|
||||||
return nodemailer.createTransport(options)
|
return nodemailer.createTransport(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getLinkCode(purpose, email, user) {
|
||||||
|
switch (purpose) {
|
||||||
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||||
|
return getResetPasswordCode(user._id)
|
||||||
|
case EmailTemplatePurpose.INVITATION:
|
||||||
|
return getInviteCode(email)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
||||||
|
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
||||||
|
* @param {string} email the address which it is being sent to for contextual purposes.
|
||||||
|
* @param {object|null} user If being sent to an existing user then the object can be provided for context.
|
||||||
|
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
// if there is a link code needed this will retrieve it
|
||||||
|
const code = await getLinkCode(purpose, email, user)
|
||||||
|
const context = {
|
||||||
|
...(await getSettingsTemplateContext(purpose, code)),
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for finding most valid SMTP configuration.
|
||||||
|
* @param {object} db The CouchDB database which is to be looked up within.
|
||||||
|
* @param {string|null} groupId If using finer grain control of configs a group can be used.
|
||||||
|
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
||||||
|
*/
|
||||||
|
async function getSmtpConfiguration(db, groupId = null) {
|
||||||
|
const params = {
|
||||||
|
type: Configs.SMTP,
|
||||||
|
}
|
||||||
|
if (groupId) {
|
||||||
|
params.group = groupId
|
||||||
|
}
|
||||||
|
return getScopedConfig(db, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a SMTP config exists based on passed in parameters.
|
||||||
|
* @param groupId
|
||||||
|
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
||||||
|
*/
|
||||||
|
exports.isEmailConfigured = async (groupId = null) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const config = await getSmtpConfiguration(db, groupId)
|
||||||
|
return config != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an email address and an email purpose this will retrieve the SMTP configuration and
|
||||||
|
* 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|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.
|
||||||
|
*/
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
const transport = createSMTPTransport(config)
|
||||||
|
const message = {
|
||||||
|
from: config.from,
|
||||||
|
subject: config.subject,
|
||||||
|
to: email,
|
||||||
|
html: await buildEmail(purpose, email, user),
|
||||||
|
}
|
||||||
|
return transport.sendMail(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an SMTP configuration this runs it through nodemailer to see if it is infact functional.
|
||||||
|
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
|
||||||
|
* @return {Promise<boolean>} returns true if the configuration is valid.
|
||||||
|
*/
|
||||||
exports.verifyConfig = async config => {
|
exports.verifyConfig = async config => {
|
||||||
const transport = exports.createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
await transport.verify()
|
await transport.verify()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
const { Client, utils } = require("@budibase/auth").redis
|
||||||
|
const { newid } = require("@budibase/auth").utils
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeACode(db, value) {
|
||||||
|
const client = await getClient(db)
|
||||||
|
const code = newid()
|
||||||
|
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).
|
||||||
|
* @param {string} userId the ID of the user which is to be reset.
|
||||||
|
* @return {Promise<string>} returns the code that was stored to redis.
|
||||||
|
*/
|
||||||
|
exports.getResetPasswordCode = async userId => {
|
||||||
|
return writeACode(utils.Databases.PW_RESETS, 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 - defaults to true.
|
||||||
|
* @return {Promise<string>} returns the user ID if it is found
|
||||||
|
*/
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an invitation code and writes it to redis - which can later be checked for user creation.
|
||||||
|
* @param {string} email the email address which the code is being sent to (for use later).
|
||||||
|
* @return {Promise<string>} returns the code that was stored to redis.
|
||||||
|
*/
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,28 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const { getConfigParams, StaticDatabases } = require("@budibase/auth").db
|
const { getScopedConfig, StaticDatabases } = require("@budibase/auth").db
|
||||||
const { Configs, TemplateBindings, LOGO_URL } = require("../constants")
|
const {
|
||||||
|
Configs,
|
||||||
|
TemplateBindings,
|
||||||
|
LOGO_URL,
|
||||||
|
EmailTemplatePurpose,
|
||||||
|
} = require("../constants")
|
||||||
const { checkSlashesInUrl } = require("./index")
|
const { checkSlashesInUrl } = require("./index")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const LOCAL_URL = `http://localhost:${env.PORT}`
|
const LOCAL_URL = `http://localhost:${env.PORT}`
|
||||||
const BASE_COMPANY = "Budibase"
|
const BASE_COMPANY = "Budibase"
|
||||||
|
|
||||||
exports.getSettingsTemplateContext = async () => {
|
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
const response = await db.allDocs(
|
// TODO: use more granular settings in the future if required
|
||||||
getConfigParams(Configs.SETTINGS, {
|
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
let settings = response.rows.map(row => row.doc)[0] || {}
|
|
||||||
if (!settings.platformUrl) {
|
if (!settings.platformUrl) {
|
||||||
settings.platformUrl = LOCAL_URL
|
settings.platformUrl = LOCAL_URL
|
||||||
}
|
}
|
||||||
// TODO: need to fully spec out the context
|
|
||||||
const URL = settings.platformUrl
|
const URL = settings.platformUrl
|
||||||
return {
|
const context = {
|
||||||
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
||||||
[TemplateBindings.PLATFORM_URL]: URL,
|
[TemplateBindings.PLATFORM_URL]: URL,
|
||||||
[TemplateBindings.REGISTRATION_URL]: checkSlashesInUrl(
|
|
||||||
`${URL}/registration`
|
|
||||||
),
|
|
||||||
[TemplateBindings.RESET_URL]: checkSlashesInUrl(`${URL}/reset`),
|
|
||||||
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
||||||
[TemplateBindings.DOCS_URL]:
|
[TemplateBindings.DOCS_URL]:
|
||||||
settings.docsUrl || "https://docs.budibase.com/",
|
settings.docsUrl || "https://docs.budibase.com/",
|
||||||
|
@ -34,4 +30,20 @@ exports.getSettingsTemplateContext = async () => {
|
||||||
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
||||||
[TemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
|
[TemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
|
||||||
}
|
}
|
||||||
|
// attach purpose specific context
|
||||||
|
switch (purpose) {
|
||||||
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||||
|
context[TemplateBindings.RESET_CODE] = code
|
||||||
|
context[TemplateBindings.RESET_URL] = checkSlashesInUrl(
|
||||||
|
`${URL}/reset?code=${code}`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case EmailTemplatePurpose.INVITATION:
|
||||||
|
context[TemplateBindings.INVITE_CODE] = code
|
||||||
|
context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl(
|
||||||
|
`${URL}/invite?code=${code}`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return context
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue