1
0
Fork 0
mirror of synced 2024-07-03 13:30:46 +12:00

Merge pull request #1383 from Budibase/feature/global-user-management

Feature/global user management
This commit is contained in:
Martin McKeaveney 2021-04-16 14:02:04 +01:00 committed by GitHub
commit a1bcb2f857
127 changed files with 3627 additions and 1133 deletions

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
packages/server/builder/**/*.js

View file

@ -35,9 +35,10 @@ services:
environment:
SELF_HOSTED: 1
PORT: 4003
JWT_SECRET: ${JWT_SECRET}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
RAW_MINIO_URL: http://minio-service:9000
MINIO_URL: http://minio-service:9000
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984

View file

@ -26,6 +26,10 @@ static_resources:
cluster: redis-service
prefix_rewrite: "/"
- match: { prefix: "/api/admin/" }
route:
cluster: worker-dev
- match: { prefix: "/api/" }
route:
cluster: server-dev
@ -42,6 +46,10 @@ static_resources:
route:
cluster: builder-dev
prefix_rewrite: "/builder/"
# special case in dev to redirect no path to builder
- match: { path: "/" }
redirect: { path_redirect: "/builder/" }
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
@ -123,3 +131,17 @@ static_resources:
address: {{ address }}
port_value: 3000
- name: worker-dev
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: worker-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: {{ address }}
port_value: 4002

View file

@ -25,6 +25,11 @@ static_resources:
- match: { path: "/" }
route:
cluster: app-service
# special case for worker admin API
- match: { path: "/api/admin" }
route:
cluster: worker-service
# special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }

View file

@ -29,7 +29,7 @@
"clean": "lerna clean",
"kill-port": "kill-port 4001",
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "lerna link && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server",
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"test": "lerna run test",
"lint": "eslint packages",
"lint:fix": "eslint --fix packages",

117
packages/auth/.gitignore vendored Normal file
View file

@ -0,0 +1,117 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

1
packages/auth/README.md Normal file
View file

@ -0,0 +1 @@
# Budibase Authentication Library

View file

@ -0,0 +1,17 @@
{
"name": "@budibase/auth",
"version": "0.0.1",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",
"license": "AGPL-3.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4",
"passport-google-auth": "^1.0.2",
"passport-google-oauth": "^2.0.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0"
}
}

View file

@ -0,0 +1,9 @@
exports.UserStatus = {
ACTIVE: "active",
INACTIVE: "inactive",
}
exports.Cookies = {
CurrentApp: "budibase:currentapp",
Auth: "budibase:auth",
}

View file

@ -0,0 +1,5 @@
module.exports.setDB = pouch => {
module.exports.CouchDB = pouch
}
module.exports.CouchDB = null

View file

@ -0,0 +1,44 @@
exports.StaticDatabases = {
USER: {
name: "user-db",
},
}
const DocumentTypes = {
USER: "us",
APP: "app",
}
exports.DocumentTypes = DocumentTypes
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.SEPARATOR = SEPARATOR
/**
* Generates a new user ID based on the passed in email.
* @param {string} email The email which the ID is going to be built up of.
* @returns {string} The new user ID which the user doc can be stored under.
*/
exports.generateUserID = email => {
return `${DocumentTypes.USER}${SEPARATOR}${email}`
}
exports.getEmailFromUserID = userId => {
return userId.split(`${DocumentTypes.USER}${SEPARATOR}`)[1]
}
/**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
exports.getUserParams = (email = "", otherProps = {}) => {
if (!email) {
email = ""
}
return {
...otherProps,
startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`,
endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`,
}
}

View file

@ -0,0 +1,8 @@
module.exports = {
JWT_SECRET: process.env.JWT_SECRET,
COUCH_DB_URL: process.env.COUCH_DB_URL,
SALT_ROUNDS: process.env.SALT_ROUNDS,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL,
}

View file

@ -0,0 +1,13 @@
const bcrypt = require("bcryptjs")
const env = require("./environment")
const SALT_ROUNDS = env.SALT_ROUNDS || 10
exports.hash = async data => {
const salt = await bcrypt.genSalt(SALT_ROUNDS)
return bcrypt.hash(data, salt)
}
exports.compare = async (data, encrypted) => {
return bcrypt.compare(data, encrypted)
}

View file

@ -0,0 +1,61 @@
const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
// const GoogleStrategy = require("passport-google-oauth").Strategy
const database = require("./db")
const { StaticDatabases } = require("./db/utils")
const { jwt, local, authenticated } = require("./middleware")
const { Cookies, UserStatus } = require("./constants")
const { hash, compare } = require("./hashing")
const {
getAppId,
setCookie,
getCookie,
clearCookie,
isClient,
} = require("./utils")
const {
generateUserID,
getUserParams,
getEmailFromUserID,
} = require("./db/utils")
// Strategies
passport.use(new LocalStrategy(local.options, local.authenticate))
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
// passport.use(new GoogleStrategy(google.options, google.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser(async (user, done) => {
const db = new database.CouchDB(StaticDatabases.USER.name)
try {
const user = await db.get(user._id)
return done(null, user)
} catch (err) {
console.error("User not found", err)
return done(null, false, { message: "User not found" })
}
})
module.exports = {
init(pouch) {
database.setDB(pouch)
},
passport,
Cookies,
UserStatus,
StaticDatabases,
generateUserID,
getUserParams,
getEmailFromUserID,
hash,
compare,
getAppId,
setCookie,
getCookie,
clearCookie,
authenticated,
isClient,
}

View file

@ -0,0 +1,21 @@
const { Cookies } = require("../constants")
const { getCookie } = require("../utils")
const { getEmailFromUserID } = require("../db/utils")
module.exports = async (ctx, next) => {
try {
// check the actual user is authenticated first
const authCookie = getCookie(ctx, Cookies.Auth)
if (authCookie) {
ctx.isAuthenticated = true
ctx.user = authCookie
// make sure email is correct from ID
ctx.user.email = getEmailFromUserID(authCookie.userId)
}
await next()
} catch (err) {
ctx.throw(err.status || 403, err)
}
}

View file

@ -0,0 +1,11 @@
const jwt = require("./passport/jwt")
const local = require("./passport/local")
const google = require("./passport/google")
const authenticated = require("./authenticated")
module.exports = {
google,
jwt,
local,
authenticated,
}

View file

@ -0,0 +1,12 @@
const env = require("../../environment")
exports.options = {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
callbackURL: env.GOOGLE_AUTH_CALLBACK_URL,
}
// exports.authenticate = async function(token, tokenSecret, profile, done) {
// // retrieve user ...
// fetchUser().then(user => done(null, user))
// }

View file

@ -0,0 +1,17 @@
const { Cookies } = require("../../constants")
const env = require("../../environment")
exports.options = {
secretOrKey: env.JWT_SECRET,
jwtFromRequest: function(ctx) {
return ctx.cookies.get(Cookies.Auth)
},
}
exports.authenticate = async function(jwt, done) {
try {
return done(null, jwt)
} catch (err) {
return done(new Error("JWT invalid."), false)
}
}

View file

@ -0,0 +1,57 @@
const jwt = require("jsonwebtoken")
const { UserStatus } = require("../../constants")
const database = require("../../db")
const { StaticDatabases, generateUserID } = require("../../db/utils")
const { compare } = require("../../hashing")
const env = require("../../environment")
const INVALID_ERR = "Invalid Credentials"
exports.options = {}
/**
* Passport Local Authentication Middleware.
* @param {*} username - username to login with
* @param {*} password - plain text password to log in with
* @param {*} done - callback from passport to return user information and errors
* @returns The authenticated user, or errors if they occur
*/
exports.authenticate = async function(username, password, done) {
if (!username) return done(null, false, "Email Required.")
if (!password) return done(null, false, "Password Required.")
// Check the user exists in the instance DB by email
const db = new database.CouchDB(StaticDatabases.USER.name)
let dbUser
try {
dbUser = await db.get(generateUserID(username))
} catch (err) {
console.error("User not found", err)
return done(null, false, { message: "User not found" })
}
// check that the user is currently inactive, if this is the case throw invalid
if (dbUser.status === UserStatus.INACTIVE) {
return done(null, false, { message: INVALID_ERR })
}
// authenticate
if (await compare(password, dbUser.password)) {
const payload = {
userId: dbUser._id,
builder: dbUser.builder,
email: dbUser.email,
}
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
expiresIn: "1 day",
})
// Remove users password in payload
delete dbUser.password
return done(null, dbUser)
} else {
done(new Error(INVALID_ERR), false)
}
}

View file

@ -0,0 +1,99 @@
const { DocumentTypes, SEPARATOR } = require("./db/utils")
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
function confirmAppId(possibleAppId) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
? possibleAppId
: undefined
}
/**
* Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through.
* @returns {string|undefined} If an appId was found it will be returned.
*/
exports.getAppId = ctx => {
const options = [ctx.headers["x-budibase-app-id"], ctx.params.appId]
if (ctx.subdomains) {
options.push(ctx.subdomains[1])
}
let appId
for (let option of options) {
appId = confirmAppId(option)
if (appId) {
break
}
}
// look in body if can't find it in subdomain
if (!appId && ctx.request.body && ctx.request.body.appId) {
appId = confirmAppId(ctx.request.body.appId)
}
let appPath =
ctx.request.headers.referrer ||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
if (!appId && appPath.length !== 0) {
appId = confirmAppId(appPath[0])
}
return appId
}
/**
* Get a cookie from context, and decrypt if necessary.
* @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to get.
*/
exports.getCookie = (ctx, name) => {
const cookie = ctx.cookies.get(name)
if (!cookie) {
return cookie
}
return jwt.verify(cookie, options.secretOrKey)
}
/**
* Store a cookie for the request, has a hardcoded expiry.
* @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to set.
* @param {string|object} value The value of cookie which will be set.
*/
exports.setCookie = (ctx, value, name = "builder") => {
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!value) {
ctx.cookies.set(name)
} else {
value = jwt.sign(value, options.secretOrKey, {
expiresIn: "1 day",
})
ctx.cookies.set(name, value, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
}
}
/**
* Utility function, simply calls setCookie with an empty string for value
*/
exports.clearCookie = (ctx, name) => {
exports.setCookie(ctx, "", name)
}
/**
* Checks if the API call being made (based on the provided ctx object) is from the client. If
* the call is not from a client app then it is from the builder.
* @param {object} ctx The koa context object to be tested.
* @return {boolean} returns true if the call is from the client lib (a built app rather than the builder).
*/
exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client"
}

594
packages/auth/yarn.lock Normal file
View file

@ -0,0 +1,594 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ajv@^6.12.3:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
dependencies:
safer-buffer "~2.1.0"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
async@~2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
integrity sha1-5YfGhYCZSsZ/xW/4bTrFa9voELw=
dependencies:
lodash "^4.14.0"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
aws4@^1.8.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
base64url@3.x.x:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
dependencies:
tweetnacl "^0.14.3"
bcryptjs@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
buffer-equal-constant-time@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"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
dependencies:
assert-plus "^1.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
dependencies:
assert-plus "^1.0.0"
google-auth-library@~0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
integrity sha1-bhW6vuhf0d0U2NEoopW2g41SE24=
dependencies:
gtoken "^1.2.1"
jws "^3.1.4"
lodash.noop "^3.0.1"
request "^2.74.0"
google-p12-pem@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177"
integrity sha1-M8RqsCGqc0+gMys5YKmj/8svMXc=
dependencies:
node-forge "^0.7.1"
googleapis@^16.0.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576"
integrity sha1-Dxny1wVy2RiIGg9ibjsaL6hilXY=
dependencies:
async "~2.1.4"
google-auth-library "~0.10.0"
string-template "~1.0.0"
gtoken@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8"
integrity sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w==
dependencies:
google-p12-pem "^0.1.0"
jws "^3.0.0"
mime "^1.4.1"
request "^2.72.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
har-validator@~5.1.3:
version "5.1.5"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
dependencies:
ajv "^6.12.3"
har-schema "^2.0.0"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsonwebtoken@^8.2.0, jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.10.0"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
koa-passport@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa"
integrity sha512-dJBCkl4X+zdYxbI2V2OtoGy0PUenpvp2ZLLWObc8UJhsId0iQpTFT8RVcuA0709AL2txGwRHnSPoT1bYNGa6Kg==
dependencies:
passport "^0.4.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.noop@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c"
integrity sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.14.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
mime-db@1.47.0:
version "1.47.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.30"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d"
integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==
dependencies:
mime-db "1.47.0"
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
node-forge@^0.7.1:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
passport-google-auth@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938"
integrity sha1-izALWqRC70M94dgy7TESh30LKTg=
dependencies:
googleapis "^16.0.0"
passport-strategy "1.x"
passport-google-oauth1@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc"
integrity sha1-r3SoA99R7GRvZqRNgigr5vEI4Mw=
dependencies:
passport-oauth1 "1.x.x"
passport-google-oauth20@2.x.x:
version "2.0.0"
resolved "https://registry.yarnpkg.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef"
integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==
dependencies:
passport-oauth2 "1.x.x"
passport-google-oauth@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz#f6eb4bc96dd6c16ec0ecfdf4e05ec48ca54d4dae"
integrity sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA==
dependencies:
passport-google-oauth1 "1.x.x"
passport-google-oauth20 "2.x.x"
passport-jwt@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065"
integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==
dependencies:
jsonwebtoken "^8.2.0"
passport-strategy "^1.0.0"
passport-local@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=
dependencies:
passport-strategy "1.x.x"
passport-oauth1@1.x.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918"
integrity sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg=
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
utils-merge "1.x.x"
passport-oauth2@1.x.x:
version "1.5.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108"
integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==
dependencies:
base64url "3.x.x"
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
passport@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
dependencies:
passport-strategy "1.x.x"
pause "0.0.1"
pause@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
psl@^1.1.28:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
request@^2.72.0, request@^2.74.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
string-template@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
utils-merge@1.x.x:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"

View file

@ -1,5 +1,6 @@
context("Create an Application", () => {
it("should create a new application", () => {
cy.login()
cy.createTestApp()
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.contains("Cypress Tests").should("exist")

View file

@ -1,5 +1,6 @@
context("Create a automation", () => {
before(() => {
cy.login()
cy.createTestApp()
})

View file

@ -1,5 +1,6 @@
context("Create Bindings", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
@ -9,7 +10,7 @@ context("Create Bindings", () => {
addSettingBinding("text", "Current User._id")
cy.getComponent(componentId).should(
"have.text",
`ro_ta_users_us_test@test.com`
`ro_ta_users_test@test.com`
)
})
})

View file

@ -2,6 +2,7 @@ context("Create Components", () => {
let headlineId
before(() => {
cy.login()
cy.createTestApp()
cy.createTable("dog")
cy.addColumn("dog", "name", "string")

View file

@ -1,5 +1,6 @@
context("Screen Tests", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})

View file

@ -1,5 +1,6 @@
context("Create a Table", () => {
before(() => {
cy.login()
cy.createTestApp()
})

View file

@ -1,5 +1,6 @@
context("Create a User", () => {
before(() => {
cy.login()
cy.createTestApp()
})

View file

@ -1,5 +1,6 @@
context("Create a View", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.createTable("data")
cy.addColumn("data", "group", "Text")

View file

@ -3,13 +3,16 @@ const path = require("path")
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
const WORKER_PORT = "4002"
const MAIN_PORT = cypressConfig.env.PORT
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
process.env.NODE_ENV = "cypress"
process.env.ENABLE_ANALYTICS = "false"
process.env.PORT = cypressConfig.env.PORT
process.env.PORT = MAIN_PORT
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = 1
process.env.WORKER_URL = "http://localhost:4002/"
process.env.MINIO_URL = "http://localhost:10000/"
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"
@ -25,18 +28,12 @@ async function run() {
// dont make this a variable or top level require
// it will cause environment module to be loaded prematurely
const server = require("../../server/src/app")
process.env.PORT = WORKER_PORT
const worker = require("../../worker/src/index")
// reload main port for rest of system
process.env.PORT = MAIN_PORT
server.on("close", () => console.log("Server Closed"))
worker.on("close", () => console.log("Worker Closed"))
}
run()
// TODO: ensure that this still works
// initialiseBudibase({ dir: homedir, clientId: "cypress-test" })
// .then(() => {
// delete require.cache[require.resolve("../../server/src/environment")]
// const xPlatHomeDir = homedir.startsWith("~")
// ? join(homedir(), homedir.substring(1))
// : homedir
// run(xPlatHomeDir)
// })
// .catch(e => console.error(e))

View file

@ -24,6 +24,23 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add("login", () => {
cy.getCookie("budibase:auth").then(cookie => {
// Already logged in
if (cookie) return
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.contains("Create Test User").click()
cy.get("input")
.first()
.type("test@test.com")
cy.get('input[type="password"]').type("test")
cy.contains("Login").click()
})
})
Cypress.Commands.add("createApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
// wait for init API calls on visit
@ -44,12 +61,6 @@ Cypress.Commands.add("createApp", name => {
.type(name)
.should("have.value", name)
cy.contains("Next").click()
cy.get("input[name=email]")
.click()
.type("test@test.com")
cy.get("input[name=password]")
.click()
.type("test")
cy.contains("Submit").click()
cy.get("[data-cy=new-table]", {
timeout: 20000,

View file

@ -1,3 +1,3 @@
Cypress.Cookies.defaults({
preserve: "budibase:builder:local",
preserve: "budibase:auth",
})

View file

@ -1,5 +1,6 @@
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies"
const apiCall = method => async (
url,
@ -8,11 +9,15 @@ const apiCall = method => async (
) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
const json = headers["Content-Type"] === "application/json"
return await fetch(url, {
const resp = await fetch(url, {
method: method,
body: json ? JSON.stringify(body) : body,
headers,
})
if (resp.status === 403) {
removeCookie(Cookies.Auth)
}
return resp
}
export const post = apiCall("POST")
@ -20,9 +25,6 @@ export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export const getBuilderCookie = async () => {
await post("/api/builder/login", {})
}
export default {
post: apiCall("POST"),
@ -30,5 +32,4 @@ export default {
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
put: apiCall("PUT"),
getBuilderCookie,
}

View file

@ -0,0 +1,16 @@
export const Cookies = {
Auth: "budibase:auth",
CurrentApp: "budibase:currentapp",
}
export function getCookie(cookieName) {
return document.cookie.split(";").some(cookie => {
return cookie.trim().startsWith(`${cookieName}=`)
})
}
export function removeCookie(cookieName) {
if (getCookie(cookieName)) {
document.cookie = `${cookieName}=; Max-Age=-99999999;`
}
}

View file

@ -6,7 +6,6 @@ import { derived, writable } from "svelte/store"
import analytics from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./storeUtils"
import { getBuilderCookie } from "./api"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
@ -58,8 +57,6 @@ export const selectedAccessRole = writable("BASIC")
export const initialise = async () => {
try {
// TODO this needs to be replaced by a real login
await getBuilderCookie()
await analytics.activate()
analytics.captureEvent("Builder Started")
} catch (err) {

View file

@ -1,7 +1,7 @@
import api from "builderStore/api"
export async function createUser(user) {
const CREATE_USER_URL = `/api/users`
const CREATE_USER_URL = `/api/users/metadata`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
}
@ -15,8 +15,7 @@ export async function saveRow(row, tableId) {
export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}`
const response = await api.delete(DELETE_ROWS_URL)
return response
return api.delete(DELETE_ROWS_URL)
}
export async function fetchDataForView(view) {

View file

@ -0,0 +1,56 @@
<script>
import { Button, Label, Input, Spacer } from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { auth } from "stores/backend"
let username = ""
let password = ""
async function login() {
try {
await auth.login({
username,
password,
})
notifier.success("Logged in successfully.")
} catch (err) {
console.error(err)
notifier.danger("Invalid credentials")
}
}
async function createTestUser() {
try {
await auth.createUser({
email: "test@test.com",
password: "test",
roles: {},
builder: {
global: true,
},
})
notifier.success("Test user created")
} catch (err) {
console.error(err)
notifier.danger("Could not create test user")
}
}
</script>
<form on:submit|preventDefault data-cy="login-form">
<Spacer large />
<Label small>Email</Label>
<Input outline bind:value={username} />
<Spacer large />
<Label small>Password</Label>
<Input outline type="password" on:change bind:value={password} />
<Spacer large />
<Button primary on:click={login}>Login</Button>
<Button secondary on:click={createTestUser}>Create Test User</Button>
</form>
<style>
form {
width: 60%;
}
</style>

View file

@ -0,0 +1 @@
export { LoginForm } from "./LoginForm.svelte"

View file

@ -25,10 +25,6 @@
applicationName: string().required("Your application must have a name."),
}
const userValidation = {
email: string()
.email()
.required("Your application needs a first user."),
password: string().required("Please enter a password for your first user."),
roleId: string().required("You need to select a role for your user."),
}
@ -153,11 +149,9 @@
// Create user
const user = {
email: $createAppStore.values.email,
password: $createAppStore.values.password,
roleId: $createAppStore.values.roleId,
}
const userResp = await api.post(`/api/users`, user)
const userResp = await api.post(`/api/users/metadata`, user)
const json = await userResp.json()
$goto(`./${appJson._id}`)
} catch (error) {

View file

@ -0,0 +1,28 @@
<script>
import { TextButton as Button, Modal } from "@budibase/bbui"
import { auth } from "stores/backend"
</script>
<div>
<Button text on:click={auth.logout}>
<i class="ri-logout-box-line" />
<p>Logout</p>
</Button>
</div>
<style>
div i {
font-size: 26px;
color: var(--grey-7);
margin-left: 12px;
margin-top: 10px;
}
div p {
font-family: var(--font-sans);
font-size: var(--font-size-s);
color: var(--ink);
font-weight: 400;
margin: 0 0 0 12px;
}
</style>

View file

@ -1,26 +1,9 @@
<script>
import { Input, Select } from "@budibase/bbui"
export let validationErrors
let blurred = { email: false, password: false }
</script>
<h2>Create your first User</h2>
<h2>What's your role for this app?</h2>
<div class="container">
<Input
on:input={() => (blurred.email = true)}
label="Email"
name="email"
placeholder="Email"
type="email"
error={blurred.email && validationErrors.email} />
<Input
on:input={() => (blurred.password = true)}
label="Password"
name="password"
placeholder="Password"
type="password"
error={blurred.password && validationErrors.password} />
<Select label="Role" secondary name="roleId">
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>

View file

@ -11,9 +11,7 @@
{#if $datasources.list.length === 0}
<i>Connect your first datasource to start building.</i>
{:else}
<i>Select a datasource to edit</i>
{/if}
{:else}<i>Select a datasource to edit</i>{/if}
<style>
i {

View file

@ -7,44 +7,56 @@
CommunityIcon,
BugIcon,
} from "components/common/Icons"
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 { auth } from "stores/backend"
let modal
</script>
<div class="root">
<div class="ui-nav">
<div class="home-logo"><img src={Logo} alt="Budibase icon" /></div>
<div class="nav-section">
<div class="nav-top">
<Link icon={AppsIcon} title="Apps" href="/" active />
<Link
icon={HostingIcon}
title="Hosting"
href="https://portal.budi.live/" />
<Link
icon={DocumentationIcon}
title="Documentation"
href="https://docs.budibase.com/" />
<Link
icon={CommunityIcon}
title="Community"
href="https://github.com/Budibase/budibase/discussions" />
<Link
icon={BugIcon}
title="Raise an issue"
href="https://github.com/Budibase/budibase/issues/new/choose" />
{#if $auth}
{#if $auth.user}
<div class="root">
<div class="ui-nav">
<div class="home-logo"><img src={Logo} alt="Budibase icon" /></div>
<div class="nav-section">
<div class="nav-top">
<Link icon={AppsIcon} title="Apps" href="/" active />
<Link
icon={HostingIcon}
title="Hosting"
href="https://portal.budi.live/" />
<Link
icon={DocumentationIcon}
title="Documentation"
href="https://docs.budibase.com/" />
<Link
icon={CommunityIcon}
title="Community"
href="https://github.com/Budibase/budibase/discussions" />
<Link
icon={BugIcon}
title="Raise an issue"
href="https://github.com/Budibase/budibase/issues/new/choose" />
</div>
<div class="nav-bottom">
<BuilderSettingsButton />
<LogoutButton />
</div>
</div>
</div>
<div class="nav-bottom">
<BuilderSettingsButton />
<div class="main">
<slot />
</div>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
{:else}
<section class="login">
<LoginForm />
</section>
{/if}
{/if}
<style>
.root {
@ -55,6 +67,14 @@
background: var(--grey-1);
}
.login {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.main {
grid-column: 2;
overflow: auto;

View file

@ -0,0 +1,43 @@
import { writable } from "svelte/store"
import api from "../../builderStore/api"
async function checkAuth() {
const response = await api.get("/api/self")
const user = await response.json()
if (response.status === 200) return user
return null
}
export function createAuthStore() {
const { subscribe, set } = writable(null)
checkAuth()
.then(user => set({ user }))
.catch(() => set({ user: null }))
return {
subscribe,
login: async creds => {
const response = await api.post(`/api/admin/auth`, creds)
const json = await response.json()
if (response.status === 200) {
set({ user: json.user })
} else {
throw "Invalid credentials"
}
return json
},
logout: async () => {
const response = await api.post(`/api/admin/auth/logout`)
await response.json()
set({ user: null })
},
createUser: async user => {
const response = await api.post(`/api/admin/users`, user)
await response.json()
},
}
}
export const auth = createAuthStore()

View file

@ -7,3 +7,4 @@ export { roles } from "./roles"
export { datasources } from "./datasources"
export { integrations } from "./integrations"
export { queries } from "./queries"
export { auth } from "./auth"

View file

@ -13,8 +13,8 @@ export const logIn = async ({ email, password }) => {
return API.error("Please enter your password")
}
return await API.post({
url: "/api/authenticate",
body: { email, password },
url: "/api/admin/auth",
body: { username: email, password },
})
}

View file

@ -19,8 +19,8 @@ const createAuthStore = () => {
// Logs a user in
const logIn = async ({ email, password }) => {
const user = await API.logIn({ email, password })
if (!user.error) {
const auth = await API.logIn({ email, password })
if (auth.success) {
await fetchUser()
await initialise()
goToDefaultRoute()
@ -30,12 +30,7 @@ const createAuthStore = () => {
// Logs a user out
const logOut = async () => {
store.set(null)
const appId = get(builderStore).appId
if (appId) {
for (let environment of ["local", "cloud"]) {
window.document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
}
}
window.document.cookie = `budibase:auth=; budibase:currentapp=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
await initialise()
goToDefaultRoute()
}

View file

@ -10,8 +10,15 @@ module.exports = async (url, opts) => {
}
}
if (url.includes("/api/admin")) {
return json({
email: "test@test.com",
_id: "us_test@test.com",
status: "active",
})
}
// mocked data based on url
if (url.includes("api/apps")) {
else if (url.includes("api/apps")) {
return json({
app1: {
url: "/app1",

View file

@ -43,7 +43,6 @@
"electron": "electron src/electron.js",
"build:electron": "electron-builder --dir",
"publish:electron": "electron-builder -mwl --publish always",
"postinstall": "electron-builder install-app-deps",
"lint": "eslint --fix src/",
"initialise": "node scripts/initialise.js"
},
@ -80,6 +79,7 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.0.1",
"@budibase/client": "^0.8.14",
"@budibase/string-templates": "^0.8.14",
"@elastic/elasticsearch": "7.10.0",

View file

@ -33,9 +33,6 @@ async function init() {
fs.writeFileSync(envoyOutputPath, processStringSync(contents, config))
const envFilePath = path.join(process.cwd(), ".env")
if (fs.existsSync(envFilePath)) {
return
}
const envFileJson = {
PORT: 4001,
MINIO_URL: "http://localhost:10000/",
@ -70,7 +67,11 @@ async function nuke() {
console.log(
"Clearing down your budibase dev environment, including all containers and volumes... 💥"
)
await compose.down(CONFIG)
await compose.down({
...CONFIG,
// stop containers, delete volumes
commandOptions: ["-v", "--remove-orphans"],
})
}
const managementCommand = process.argv.slice(2)[0]

View file

@ -1,6 +1,5 @@
const CouchDB = require("../../db")
const env = require("../../environment")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const packageJson = require("../../../package.json")
const {
createLinkView,
@ -74,7 +73,7 @@ async function getAppUrlIfNotInUse(ctx) {
if (!env.SELF_HOSTED) {
return url
}
const deployedApps = await getDeployedApps()
const deployedApps = await getDeployedApps(ctx)
if (
deployedApps[url] != null &&
deployedApps[url].appId !== ctx.params.appId
@ -145,7 +144,6 @@ exports.fetchAppPackage = async function(ctx) {
layouts,
clientLibPath: clientLibraryPath(ctx.params.appId),
}
await setBuilderToken(ctx, ctx.params.appId, application.version)
}
exports.create = async function(ctx) {
@ -161,7 +159,6 @@ exports.create = async function(ctx) {
const url = await getAppUrlIfNotInUse(ctx)
const appId = instance._id
const version = packageJson.version
const newApplication = {
_id: appId,
type: "app",
@ -184,7 +181,6 @@ exports.create = async function(ctx) {
await createApp(appId)
}
await setBuilderToken(ctx, appId, version)
ctx.status = 200
ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully`

View file

@ -3,12 +3,12 @@ const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const env = require("../../environment")
const { getAPIKey } = require("../../utilities/usageQuota")
const { generateUserID } = require("../../db/utils")
const { generateUserMetadataID } = require("../../db/utils")
const { setCookie } = require("../../utilities")
const { outputProcessing } = require("../../utilities/rowProcessor")
const { ViewNames } = require("../../db/utils")
const { UserStatus } = require("../../constants")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const { InternalTables } = require("../../db/utils")
const { UserStatus } = require("@budibase/auth")
const { getFullUser } = require("../../utilities/users")
const INVALID_ERR = "Invalid Credentials"
@ -27,7 +27,7 @@ exports.authenticate = async ctx => {
let dbUser
try {
dbUser = await db.get(generateUserID(email))
dbUser = await db.get(generateUserMetadataID(email))
} catch (_) {
// do not want to throw a 404 - as this could be
// used to determine valid emails
@ -49,7 +49,7 @@ exports.authenticate = async ctx => {
// if in prod add the user api key, unless self hosted
/* istanbul ignore next */
if (env.isProd() && !env.SELF_HOSTED) {
const { apiKey } = await getAPIKey(ctx.user.appId)
const { apiKey } = await getAPIKey(ctx.appId)
payload.apiKey = apiKey
}
@ -70,24 +70,36 @@ exports.authenticate = async ctx => {
}
}
exports.builderLogin = async ctx => {
await setBuilderToken(ctx)
ctx.status = 200
}
exports.fetchSelf = async ctx => {
const { userId, appId } = ctx.user
if (!ctx.user) {
ctx.throw(403, "No user logged in")
}
const appId = ctx.appId
const { userId } = ctx.user
/* istanbul ignore next */
if (!userId || !appId) {
if (!userId) {
ctx.body = {}
return
}
const db = new CouchDB(appId)
const user = await db.get(userId)
const userTable = await db.get(ViewNames.USERS)
if (user) {
delete user.password
const user = await getFullUser({ ctx, userId: userId })
if (appId) {
const db = new CouchDB(appId)
// remove the full roles structure
delete user.roles
try {
const userTable = await db.get(InternalTables.USER_METADATA)
const metadata = await db.get(userId)
// specifically needs to make sure is enriched
ctx.body = await outputProcessing(appId, userTable, {
...user,
...metadata,
})
} catch (err) {
ctx.body = user
}
} else {
ctx.body = user
}
// specifically needs to make sure is enriched
ctx.body = await outputProcessing(appId, userTable, user)
}

View file

@ -34,13 +34,13 @@ function cleanAutomationInputs(automation) {
/**
* This function handles checking if any webhooks need to be created or deleted for automations.
* @param {object} user The user object, including all auth info
* @param {string} appId The ID of the app in which we are checking for webhooks
* @param {object|undefined} oldAuto The old automation object if updating/deleting
* @param {object|undefined} newAuto The new automation object if creating/updating
* @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be
* written to DB (this does not write to DB as it would be wasteful to repeat).
*/
async function checkForWebhooks({ user, oldAuto, newAuto }) {
async function checkForWebhooks({ appId, oldAuto, newAuto }) {
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
const newTrigger = newAuto ? newAuto.definition.trigger : null
function isWebhookTrigger(auto) {
@ -56,11 +56,11 @@ async function checkForWebhooks({ user, oldAuto, newAuto }) {
!isWebhookTrigger(newAuto) &&
oldTrigger.webhookId
) {
let db = new CouchDB(user.appId)
let db = new CouchDB(appId)
// need to get the webhook to get the rev
const webhook = await db.get(oldTrigger.webhookId)
const ctx = {
user,
appId,
params: { id: webhook._id, rev: webhook._rev },
}
// might be updating - reset the inputs to remove the URLs
@ -73,7 +73,7 @@ async function checkForWebhooks({ user, oldAuto, newAuto }) {
// need to create webhook
else if (!isWebhookTrigger(oldAuto) && isWebhookTrigger(newAuto)) {
const ctx = {
user,
appId,
request: {
body: new webhooks.Webhook(
"Automation webhook",
@ -86,17 +86,17 @@ async function checkForWebhooks({ user, oldAuto, newAuto }) {
const id = ctx.body.webhook._id
newTrigger.webhookId = id
newTrigger.inputs = {
schemaUrl: `api/webhooks/schema/${user.appId}/${id}`,
triggerUrl: `api/webhooks/trigger/${user.appId}/${id}`,
schemaUrl: `api/webhooks/schema/${appId}/${id}`,
triggerUrl: `api/webhooks/trigger/${appId}/${id}`,
}
}
return newAuto
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
let automation = ctx.request.body
automation.appId = ctx.user.appId
automation.appId = ctx.appId
// call through to update if already exists
if (automation._id && automation._rev) {
@ -107,7 +107,10 @@ exports.create = async function(ctx) {
automation.type = "automation"
automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({ user: ctx.user, newAuto: automation })
automation = await checkForWebhooks({
appId: ctx.appId,
newAuto: automation,
})
const response = await db.put(automation)
automation._rev = response.rev
@ -122,13 +125,13 @@ exports.create = async function(ctx) {
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
let automation = ctx.request.body
automation.appId = ctx.user.appId
automation.appId = ctx.appId
const oldAutomation = await db.get(automation._id)
automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({
user: ctx.user,
appId: ctx.appId,
oldAuto: oldAutomation,
newAuto: automation,
})
@ -147,7 +150,7 @@ exports.update = async function(ctx) {
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const response = await db.allDocs(
getAutomationParams(null, {
include_docs: true,
@ -157,14 +160,17 @@ exports.fetch = async function(ctx) {
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
ctx.body = await db.get(ctx.params.id)
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const oldAutomation = await db.get(ctx.params.id)
await checkForWebhooks({ user: ctx.user, oldAuto: oldAutomation })
await checkForWebhooks({
appId: ctx.appId,
oldAuto: oldAutomation,
})
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
}
@ -195,11 +201,11 @@ module.exports.getDefinitionList = async function(ctx) {
*********************/
exports.trigger = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
let automation = await db.get(ctx.params.id)
await triggers.externalTrigger(automation, {
...ctx.request.body,
appId: ctx.user.appId,
appId: ctx.appId,
})
ctx.status = 200
ctx.body = {

View file

@ -6,7 +6,7 @@ const {
} = require("../../db/utils")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const database = new CouchDB(ctx.appId)
ctx.body = (
await database.allDocs(
getDatasourceParams(null, {
@ -17,7 +17,7 @@ exports.fetch = async function(ctx) {
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const datasource = {
_id: generateDatasourceID(),
@ -34,7 +34,7 @@ exports.save = async function(ctx) {
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
// Delete all queries for the datasource
const rows = await db.allDocs(getQueryParams(ctx.params.datasourceId, null))
@ -48,6 +48,6 @@ exports.destroy = async function(ctx) {
}
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const database = new CouchDB(ctx.appId)
ctx.body = await database.get(ctx.params.datasourceId)
}

View file

@ -93,7 +93,7 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function(ctx) {
try {
const db = new PouchDB(ctx.user.appId)
const db = new PouchDB(ctx.appId)
const deploymentDoc = await db.get("_local/deployments")
const { updated, deployments } = await checkAllDeployments(
deploymentDoc,
@ -110,7 +110,7 @@ exports.fetchDeployments = async function(ctx) {
exports.deploymentProgress = async function(ctx) {
try {
const db = new PouchDB(ctx.user.appId)
const db = new PouchDB(ctx.appId)
const deploymentDoc = await db.get("_local/deployments")
ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) {
@ -128,7 +128,7 @@ exports.deployApp = async function(ctx) {
hostingInfo.type === HostingTypes.CLOUD
? require("./awsDeploy")
: require("./selfDeploy")
let deployment = new Deployment(ctx.user.appId)
let deployment = new Deployment(ctx.appId)
deployment.setStatus(DeploymentStatus.PENDING)
deployment = await storeLocalDeploymentHistory(deployment)

View file

@ -0,0 +1,34 @@
const fetch = require("node-fetch")
const env = require("../../environment")
const { checkSlashesInUrl } = require("../../utilities")
const { request } = require("../../utilities/workerRequests")
async function redirect(ctx, method) {
const { path } = ctx.params
const response = await fetch(
checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${path}`),
request(ctx, {
method,
body: ctx.request.body,
})
)
ctx.body = await response.json()
const cookie = response.headers.get("set-cookie")
if (cookie) {
ctx.set("set-cookie", cookie)
}
ctx.status = response.status
ctx.cookies
}
exports.redirectGet = async ctx => {
await redirect(ctx, "GET")
}
exports.redirectPost = async ctx => {
await redirect(ctx, "POST")
}
exports.redirectDelete = async ctx => {
await redirect(ctx, "DELETE")
}

View file

@ -40,5 +40,5 @@ exports.fetchUrls = async ctx => {
}
exports.getDeployedApps = async ctx => {
ctx.body = await getDeployedApps()
ctx.body = await getDeployedApps(ctx)
}

View file

@ -3,7 +3,7 @@ const CouchDB = require("../../db")
const { generateLayoutID, getScreenParams } = require("../../db/utils")
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
let layout = ctx.request.body
if (!layout.props) {
@ -22,7 +22,7 @@ exports.save = async function(ctx) {
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const layoutId = ctx.params.layoutId,
layoutRev = ctx.params.layoutRev

View file

@ -28,7 +28,7 @@ function formatResponse(resp) {
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const body = await db.allDocs(
getQueryParams(null, {
@ -39,7 +39,7 @@ exports.fetch = async function(ctx) {
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const query = ctx.request.body
if (!query._id) {
@ -90,7 +90,7 @@ async function enrichQueryFields(fields, parameters) {
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const query = enrichQueries(await db.get(ctx.params.queryId))
// remove properties that could be dangerous in real app
if (env.isProd()) {
@ -102,7 +102,7 @@ exports.find = async function(ctx) {
}
exports.preview = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const datasource = await db.get(ctx.request.body.datasourceId)
@ -130,7 +130,7 @@ exports.preview = async function(ctx) {
}
exports.execute = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId)
@ -153,7 +153,7 @@ exports.execute = async function(ctx) {
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
await db.remove(ctx.params.queryId, ctx.params.revId)
ctx.message = `Query deleted.`
ctx.status = 200

View file

@ -10,8 +10,8 @@ const {
const {
generateRoleID,
getRoleParams,
getUserParams,
ViewNames,
getUserMetadataParams,
InternalTables,
} = require("../../db/utils")
const UpdateRolesOptions = {
@ -28,7 +28,7 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [
]
async function updateRolesOnUserTable(db, roleId, updateOption) {
const table = await db.get(ViewNames.USERS)
const table = await db.get(InternalTables.USER_METADATA)
const schema = table.schema
const remove = updateOption === UpdateRolesOptions.REMOVED
let updated = false
@ -51,7 +51,7 @@ async function updateRolesOnUserTable(db, roleId, updateOption) {
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const body = await db.allDocs(
getRoleParams(null, {
include_docs: true,
@ -79,11 +79,11 @@ exports.fetch = async function(ctx) {
}
exports.find = async function(ctx) {
ctx.body = await getRole(ctx.user.appId, ctx.params.roleId)
ctx.body = await getRole(ctx.appId, ctx.params.roleId)
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
let { _id, name, inherits, permissionId } = ctx.request.body
if (!_id) {
_id = generateRoleID()
@ -104,7 +104,7 @@ exports.save = async function(ctx) {
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const roleId = ctx.params.roleId
if (isBuiltin(roleId)) {
ctx.throw(400, "Cannot delete builtin role.")
@ -112,7 +112,7 @@ exports.destroy = async function(ctx) {
// first check no users actively attached to role
const users = (
await db.allDocs(
getUserParams(null, {
getUserMetadataParams(null, {
include_docs: true,
})
)

View file

@ -6,10 +6,10 @@ const {
generateRowID,
DocumentTypes,
SEPARATOR,
ViewNames,
generateUserID,
InternalTables,
generateUserMetadataID,
} = require("../../db/utils")
const usersController = require("./user")
const userController = require("./user")
const {
inputProcessing,
outputProcessing,
@ -37,18 +37,14 @@ validateJs.extend(validateJs.validators.datetime, {
},
})
async function findRow(db, appId, tableId, rowId) {
async function findRow(ctx, db, tableId, rowId) {
let row
if (tableId === ViewNames.USERS) {
let ctx = {
params: {
userId: rowId,
},
user: {
appId,
},
// TODO remove special user case in future
if (tableId === InternalTables.USER_METADATA) {
ctx.params = {
userId: rowId,
}
await usersController.find(ctx)
await userController.findMetadata(ctx)
row = ctx.body
} else {
row = await db.get(rowId)
@ -60,7 +56,7 @@ async function findRow(db, appId, tableId, rowId) {
}
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
let dbRow = await db.get(ctx.params.rowId)
let dbTable = await db.get(dbRow.tableId)
@ -96,14 +92,14 @@ exports.patch = async function(ctx) {
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
// TODO remove special user case in future
if (row.tableId === InternalTables.USER_METADATA) {
// the row has been updated, need to put it into the ctx
ctx.request.body = {
...row,
password: ctx.request.body.password,
}
await usersController.update(ctx)
await userController.updateMetadata(ctx)
return
}
@ -121,7 +117,7 @@ exports.patch = async function(ctx) {
}
exports.save = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
let inputs = ctx.request.body
inputs.tableId = ctx.params.tableId
@ -134,16 +130,19 @@ exports.save = async function(ctx) {
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.rowId = inputs._id
await exports.patch(ctx)
return
if (inputs._id && inputs._rev) {
const existingRow = await db.get(inputs._id)
if (existingRow) {
ctx.params.rowId = inputs._id
await exports.patch(ctx)
return
}
}
if (!inputs._rev && !inputs._id) {
if (inputs.tableId === ViewNames.USERS) {
inputs._id = generateUserID(inputs.email)
// TODO remove special user case in future
if (inputs.tableId === InternalTables.USER_METADATA) {
inputs._id = generateUserMetadataID(inputs.email)
} else {
inputs._id = generateRowID(inputs.tableId)
}
@ -175,11 +174,11 @@ exports.save = async function(ctx) {
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
// TODO remove special user case in future
if (row.tableId === InternalTables.USER_METADATA) {
// the row has been updated, need to put it into the ctx
ctx.request.body = row
await usersController.create(ctx)
await userController.createMetadata(ctx)
return
}
@ -197,7 +196,7 @@ exports.save = async function(ctx) {
}
exports.fetchView = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
@ -256,7 +255,7 @@ exports.fetchView = async function(ctx) {
}
exports.search = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const {
query,
@ -287,14 +286,6 @@ exports.search = async function(ctx) {
}
const response = await search(searchString)
// delete passwords from users
if (tableId === ViewNames.USERS) {
for (let row of response.rows) {
delete row.password
}
}
const table = await db.get(tableId)
ctx.body = {
rows: await outputProcessing(appId, table, response.rows),
@ -303,14 +294,14 @@ exports.search = async function(ctx) {
}
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
// special case for users, fetch through the user controller
// TODO remove special user case in future
let rows,
table = await db.get(ctx.params.tableId)
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
if (ctx.params.tableId === InternalTables.USER_METADATA) {
await userController.fetchMetadata(ctx)
rows = ctx.body
} else {
const response = await db.allDocs(
@ -324,11 +315,11 @@ exports.fetchTableRows = async function(ctx) {
}
exports.find = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
try {
const table = await db.get(ctx.params.tableId)
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
const row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
ctx.body = await outputProcessing(appId, table, row)
} catch (err) {
ctx.throw(400, err)
@ -336,7 +327,7 @@ exports.find = async function(ctx) {
}
exports.destroy = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
@ -348,17 +339,25 @@ exports.destroy = async function(ctx) {
row,
tableId: row.tableId,
})
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
ctx.status = 200
// TODO remove special user case in future
if (ctx.params.tableId === InternalTables.USER_METADATA) {
ctx.params = {
userId: ctx.params.rowId,
}
await userController.destroyMetadata(ctx)
} else {
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
}
// for automations include the row that was deleted
ctx.row = row
ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
}
exports.validate = async function(ctx) {
const errors = await validate({
appId: ctx.user.appId,
appId: ctx.appId,
tableId: ctx.params.tableId,
row: ctx.request.body,
})
@ -388,14 +387,14 @@ async function validate({ appId, tableId, row, table }) {
}
exports.fetchEnrichedRow = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const tableId = ctx.params.tableId
const rowId = ctx.params.rowId
// need table to work out where links go in row
let [table, row] = await Promise.all([
db.get(tableId),
findRow(db, appId, tableId, rowId),
findRow(ctx, db, tableId, rowId),
])
// get the link docs
const linkVals = await linkRows.getLinkDocuments({
@ -433,11 +432,11 @@ exports.fetchEnrichedRow = async function(ctx) {
}
async function bulkDelete(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const { rows } = ctx.request.body
const db = new CouchDB(appId)
const linkUpdates = rows.map(row =>
let updates = rows.map(row =>
linkRows.updateLinks({
appId,
eventType: linkRows.EventType.ROW_DELETE,
@ -445,9 +444,20 @@ async function bulkDelete(ctx) {
tableId: row.tableId,
})
)
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
await Promise.all(linkUpdates)
// TODO remove special user case in future
if (ctx.params.tableId === InternalTables.USER_METADATA) {
updates = updates.concat(
rows.map(row => {
ctx.params = {
userId: row._id,
}
return userController.destroyMetadata(ctx)
})
)
} else {
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
}
await Promise.all(updates)
rows.forEach(row => {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)

View file

@ -3,7 +3,7 @@ const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/roles")
exports.fetch = async ctx => {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const screens = (
@ -21,7 +21,7 @@ exports.fetch = async ctx => {
}
exports.save = async ctx => {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
let screen = ctx.request.body
@ -39,7 +39,7 @@ exports.save = async ctx => {
}
exports.destroy = async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
await db.remove(ctx.params.screenId, ctx.params.screenRev)
ctx.body = {
message: "Screen deleted successfully",

View file

@ -1,8 +1,7 @@
const { QueryBuilder, buildSearchUrl, search } = require("./utils")
exports.rowSearch = async ctx => {
// this can't be done through pouch, have to reach for trusty node-fetch
const appId = ctx.user.appId
const appId = ctx.appId
const { tableId } = ctx.params
const { bookmark, query, raw } = ctx.request.body
let url

View file

@ -9,7 +9,6 @@ const { processString } = require("@budibase/string-templates")
const { budibaseTempDir } = require("../../../utilities/budibaseDir")
const { getDeployedApps } = require("../../../utilities/builder/hosting")
const CouchDB = require("../../../db")
const setBuilderToken = require("../../../utilities/builder/setBuilderToken")
const {
loadHandlebarsFile,
NODE_MODULES_PATH,
@ -22,7 +21,7 @@ const { objectStoreUrl, clientLibraryPath } = require("../../../utilities")
async function checkForSelfHostedURL(ctx) {
// the "appId" component of the URL may actually be a specific self hosted URL
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
const apps = await getDeployedApps()
const apps = await getDeployedApps(ctx)
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
return apps[possibleAppUrl].appId
} else {
@ -35,9 +34,6 @@ const COMP_LIB_BASE_APP_VERSION = "0.2.5"
exports.serveBuilder = async function(ctx) {
let builderPath = resolve(TOP_LEVEL_PATH, "builder")
if (ctx.file === "index.html") {
await setBuilderToken(ctx)
}
await send(ctx, ctx.file, { root: builderPath })
}
@ -60,7 +56,7 @@ exports.uploadFile = async function(ctx) {
return prepareUpload({
file,
s3Key: `assets/${ctx.user.appId}/attachments/${processedFileName}`,
s3Key: `assets/${ctx.appId}/attachments/${processedFileName}`,
bucket: "prod-budi-app-assets",
})
})
@ -110,20 +106,17 @@ exports.serveComponentLibrary = async function(ctx) {
)
return send(ctx, "/awsDeploy.js", { root: componentLibraryPath })
}
const db = new CouchDB(appId)
const appInfo = await db.get(appId)
let componentLib = "componentlibrary"
if (ctx.user.version) {
componentLib += `-${ctx.user.version}`
if (appInfo && appInfo.version) {
componentLib += `-${appInfo.version}`
} else {
componentLib += `-${COMP_LIB_BASE_APP_VERSION}`
}
const S3_URL = encodeURI(
join(
objectStoreUrl(appId),
componentLib,
ctx.query.library,
"dist",
"index.js"
)
join(objectStoreUrl(), componentLib, ctx.query.library, "dist", "index.js")
)
const response = await fetch(S3_URL)
const body = await response.text()

View file

@ -10,7 +10,7 @@ const { FieldTypes } = require("../../../constants")
const { TableSaveFunctions } = require("./utils")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const body = await db.allDocs(
getTableParams(null, {
include_docs: true,
@ -20,12 +20,12 @@ exports.fetch = async function(ctx) {
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
ctx.body = await db.get(ctx.params.id)
}
exports.save = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const { dataImport, ...rest } = ctx.request.body
let tableToSave = {
@ -127,7 +127,7 @@ exports.save = async function(ctx) {
}
exports.destroy = async function(ctx) {
const appId = ctx.user.appId
const appId = ctx.appId
const db = new CouchDB(appId)
const tableToDelete = await db.get(ctx.params.tableId)

View file

@ -1,6 +1,10 @@
const CouchDB = require("../../../db")
const csvParser = require("../../../utilities/csvParser")
const { getRowParams, generateRowID, ViewNames } = require("../../../db/utils")
const {
getRowParams,
generateRowID,
InternalTables,
} = require("../../../db/utils")
const { isEqual } = require("lodash/fp")
const { AutoFieldSubTypes } = require("../../../constants")
const { inputProcessing } = require("../../../utilities/rowProcessor")
@ -57,8 +61,8 @@ exports.makeSureTableUpToDate = (table, tableToSave) => {
return tableToSave
}
exports.handleDataImport = async (user, table, dataImport) => {
const db = new CouchDB(user.appId)
exports.handleDataImport = async (appId, user, table, dataImport) => {
const db = new CouchDB(appId)
if (dataImport && dataImport.csvString) {
// Populate the table with rows imported from CSV in a bulk update
const data = await csvParser.transform(dataImport)
@ -136,7 +140,7 @@ exports.handleSearchIndexes = async (appId, table) => {
exports.checkStaticTables = table => {
// check user schema has all required elements
if (table._id === ViewNames.USERS) {
if (table._id === InternalTables.USER_METADATA) {
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
// check if the schema exists on the table to be created/updated
if (table.schema[key] == null) {
@ -152,7 +156,7 @@ class TableSaveFunctions {
this.db = db
this.ctx = ctx
if (this.ctx && this.ctx.user) {
this.appId = this.ctx.user.appId
this.appId = this.ctx.appId
}
this.oldTable = oldTable
this.dataImport = dataImport
@ -184,6 +188,7 @@ class TableSaveFunctions {
async after(table) {
table = await exports.handleSearchIndexes(this.appId, table)
table = await exports.handleDataImport(
this.appId,
this.ctx.user,
table,
this.dataImport

View file

@ -1,115 +1,110 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const {
generateUserMetadataID,
getUserMetadataParams,
getEmailFromUserMetadataID,
} = require("../../db/utils")
const { InternalTables } = require("../../db/utils")
const { getRole } = require("../../utilities/security/roles")
const { UserStatus } = require("../../constants")
const {
getGlobalUsers,
saveGlobalUser,
deleteGlobalUser,
} = require("../../utilities/workerRequests")
const { getFullUser } = require("../../utilities/users")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const users = (
exports.fetchMetadata = async function(ctx) {
const database = new CouchDB(ctx.appId)
const global = await getGlobalUsers(ctx, ctx.appId)
const metadata = (
await database.allDocs(
getUserParams(null, {
getUserMetadataParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// user hashed password shouldn't ever be returned
for (let user of users) {
delete user.password
const users = []
for (let user of global) {
const info = metadata.find(meta => meta._id.includes(user.email))
// remove these props, not for the correct DB
delete user._id
delete user._rev
users.push({
...user,
...info,
// make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(user.email),
})
}
ctx.body = users
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const { email, password, roleId } = ctx.request.body
if (!email || !password) {
ctx.throw(400, "email and Password Required.")
}
const role = await getRole(ctx.user.appId, roleId)
exports.createMetadata = async function(ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
const { roleId } = ctx.request.body
const email = ctx.request.body.email || ctx.user.email
// check role valid
const role = await getRole(appId, roleId)
if (!role) ctx.throw(400, "Invalid Role")
const hashedPassword = await bcrypt.hash(password)
const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body)
const user = {
...ctx.request.body,
// these must all be after the object spread, make sure
// any values are overwritten, generateUserID will always
// generate the same ID for the user as it is not UUID based
_id: generateUserID(email),
...metadata,
_id: generateUserMetadataID(email),
type: "user",
password: hashedPassword,
tableId: ViewNames.USERS,
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = UserStatus.ACTIVE
tableId: InternalTables.USER_METADATA,
}
try {
const response = await db.post(user)
ctx.status = 200
ctx.message = "User created successfully."
ctx.userId = response.id
ctx.body = {
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
ctx.throw(400, "User exists already")
} else {
ctx.throw(err.status, err)
}
}
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
let dbUser
if (user.email && !user._id) {
user._id = generateUserID(user.email)
}
// get user incase password removed
if (user._id) {
dbUser = await db.get(user._id)
}
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
const response = await db.put({
password: dbUser.password,
...user,
})
user._rev = response.rev
const response = await db.post(user)
// for automations to make it obvious was successful
ctx.status = 200
ctx.body = response
ctx.body = {
_id: response.id,
_rev: response.rev,
email,
}
}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
await database.destroy(generateUserID(ctx.params.email))
exports.updateMetadata = async function(ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
const user = ctx.request.body
let email = user.email || getEmailFromUserMetadataID(user._id)
const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body)
if (!metadata._id) {
metadata._id = generateUserMetadataID(email)
}
if (!metadata._rev) {
metadata._rev = ctx.request.body._rev
}
ctx.body = await db.put({
...metadata,
})
}
exports.destroyMetadata = async function(ctx) {
const db = new CouchDB(ctx.appId)
const email =
ctx.params.email || getEmailFromUserMetadataID(ctx.params.userId)
await deleteGlobalUser(ctx, email)
try {
const dbUser = await db.get(generateUserMetadataID(email))
await db.remove(dbUser._id, dbUser._rev)
} catch (err) {
// error just means the global user has no config in this app
}
ctx.body = {
message: `User ${ctx.params.email} deleted.`,
}
ctx.status = 200
}
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
let lookup = ctx.params.email
? generateUserID(ctx.params.email)
: ctx.params.userId
const user = await database.get(lookup)
if (user) {
delete user.password
}
ctx.body = user
exports.findMetadata = async function(ctx) {
ctx.body = await getFullUser({
ctx,
email: ctx.params.email,
userId: ctx.params.userId,
})
}

View file

@ -7,7 +7,7 @@ const { ViewNames } = require("../../../db/utils")
const controller = {
fetch: async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const designDoc = await db.get("_design/database")
const response = []
@ -25,7 +25,7 @@ const controller = {
ctx.body = response
},
save: async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const { originalName, ...viewToSave } = ctx.request.body
const designDoc = await db.get("_design/database")
const view = viewTemplate(viewToSave)
@ -66,7 +66,7 @@ const controller = {
}
},
destroy: async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const designDoc = await db.get("_design/database")
const viewName = decodeURI(ctx.params.viewName)
const view = designDoc.views[viewName]
@ -81,7 +81,7 @@ const controller = {
ctx.body = view
},
exportView: async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const designDoc = await db.get("_design/database")
const viewName = decodeURI(ctx.query.view)

View file

@ -22,7 +22,7 @@ exports.WebhookType = {
}
exports.fetch = async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const response = await db.allDocs(
getWebhookParams(null, {
include_docs: true,
@ -32,9 +32,9 @@ exports.fetch = async ctx => {
}
exports.save = async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
const webhook = ctx.request.body
webhook.appId = ctx.user.appId
webhook.appId = ctx.appId
// check that the webhook exists
if (webhook._id) {
@ -51,7 +51,7 @@ exports.save = async ctx => {
}
exports.destroy = async ctx => {
const db = new CouchDB(ctx.user.appId)
const db = new CouchDB(ctx.appId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
}

View file

@ -1,5 +1,6 @@
const Router = require("@koa/router")
const authenticated = require("../middleware/authenticated")
const { authenticated } = require("@budibase/auth")
const currentApp = require("../middleware/currentapp")
const compress = require("koa-compress")
const zlib = require("zlib")
const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
@ -13,10 +14,10 @@ router
compress({
threshold: 2048,
gzip: {
flush: zlib.Z_SYNC_FLUSH,
flush: zlib.constants.Z_SYNC_FLUSH,
},
deflate: {
flush: zlib.Z_SYNC_FLUSH,
flush: zlib.constants.Z_SYNC_FLUSH,
},
br: false,
})
@ -31,6 +32,7 @@ router
.use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version))
.use(authenticated)
.use(currentApp)
// error handling middleware
router.use(async (ctx, next) => {

View file

@ -1,14 +1,8 @@
const Router = require("@koa/router")
const controller = require("../controllers/auth")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router.post("/api/authenticate", controller.authenticate)
// TODO: this is a hack simply to make sure builder has a cookie until auth reworked
router.post("/api/builder/login", authorized(BUILDER), controller.builderLogin)
// doesn't need authorization as can only fetch info about self
router.get("/api/self", controller.fetchSelf)
module.exports = router

View file

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const controller = require("../controllers/dev")
const env = require("../../environment")
const router = Router()
if (env.isDev() || env.isTest()) {
router.get("/api/admin/:path", controller.redirectGet)
router.post("/api/admin/:path", controller.redirectPost)
router.delete("/api/admin/:path", controller.redirectDelete)
}
module.exports = router

View file

@ -22,6 +22,7 @@ const datasourceRoutes = require("./datasource")
const queryRoutes = require("./query")
const hostingRoutes = require("./hosting")
const backupRoutes = require("./backup")
const devRoutes = require("./dev")
exports.mainRoutes = [
deployRoutes,
@ -44,6 +45,7 @@ exports.mainRoutes = [
queryRoutes,
hostingRoutes,
backupRoutes,
devRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View file

@ -1,5 +1,15 @@
const setup = require("./utilities")
require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {
email: "test@test.com",
}
}),
saveGlobalUser: jest.fn(),
}))
describe("/authenticate", () => {
let request = setup.getRequest()
let config = setup.getConfig()
@ -10,88 +20,8 @@ describe("/authenticate", () => {
await config.init()
})
describe("authenticate", () => {
it("should be able to create a layout", async () => {
await config.createUser("test@test.com", "p4ssw0rd")
const res = await request
.post(`/api/authenticate`)
.send({
email: "test@test.com",
password: "p4ssw0rd",
})
.set(config.publicHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.token).toBeDefined()
expect(res.body.email).toEqual("test@test.com")
expect(res.body.password).toBeUndefined()
})
it("should error if no app specified", async () => {
await request
.post(`/api/authenticate`)
.expect(400)
})
it("should error if no email specified", async () => {
await request
.post(`/api/authenticate`)
.send({
password: "test",
})
.set(config.publicHeaders())
.expect(400)
})
it("should error if no password specified", async () => {
await request
.post(`/api/authenticate`)
.send({
email: "test",
})
.set(config.publicHeaders())
.expect(400)
})
it("should error if invalid user specified", async () => {
await request
.post(`/api/authenticate`)
.send({
email: "test",
password: "test",
})
.set(config.publicHeaders())
.expect(401)
})
it("should throw same error if wrong password specified", async () => {
await config.createUser("test@test.com", "password")
await request
.post(`/api/authenticate`)
.send({
email: "test@test.com",
password: "test",
})
.set(config.publicHeaders())
.expect(401)
})
it("should throw an error for inactive users", async () => {
await config.createUser("test@test.com", "password")
await config.makeUserInactive("test@test.com")
await request
.post(`/api/authenticate`)
.send({
email: "test@test.com",
password: "password",
})
.set(config.publicHeaders())
.expect(401)
})
})
describe("fetch self", () => {
it("should be able to delete the layout", async () => {
it("should be able to fetch self", async () => {
await config.createUser("test@test.com", "p4ssw0rd")
const headers = await config.login("test@test.com", "p4ssw0rd")
const res = await request

View file

@ -41,10 +41,12 @@ describe("run misc tests", () => {
const dataImport = {
csvString: "a,b,c,d\n1,2,3,4"
}
await tableUtils.handleDataImport({
appId: config.getAppId(),
userId: "test",
}, table, dataImport)
await tableUtils.handleDataImport(
config.getAppId(),
{ userId: "test" },
table,
dataImport
)
const rows = await config.getRows()
expect(rows[0].a).toEqual("1")
expect(rows[0].b).toEqual("2")

View file

@ -2,6 +2,12 @@ const setup = require("./utilities")
const { basicScreen } = setup.structures
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const workerRequests = require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(),
saveGlobalUser: jest.fn(),
}))
const route = "/test"
@ -25,6 +31,13 @@ describe("/routing", () => {
describe("fetch", () => {
it("returns the correct routing for basic user", async () => {
workerRequests.getGlobalUsers.mockImplementationOnce((ctx, appId) => {
return {
roles: {
[appId]: BUILTIN_ROLE_IDS.BASIC,
}
}
})
const res = await request
.get(`/api/routing/client`)
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.BASIC))
@ -42,6 +55,13 @@ describe("/routing", () => {
})
it("returns the correct routing for power user", async () => {
workerRequests.getGlobalUsers.mockImplementationOnce((ctx, appId) => {
return {
roles: {
[appId]: BUILTIN_ROLE_IDS.POWER,
}
}
})
const res = await request
.get(`/api/routing/client`)
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.POWER))

View file

@ -2,6 +2,15 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const { checkPermissionsEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities")
const { basicUser } = setup.structures
const workerRequests = require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(),
saveGlobalUser: jest.fn(() => {
return {}
}),
deleteGlobalUser: jest.fn(),
}))
describe("/users", () => {
let request = setup.getRequest()
@ -14,11 +23,23 @@ describe("/users", () => {
})
describe("fetch", () => {
beforeEach(() => {
workerRequests.getGlobalUsers.mockImplementationOnce(() => ([
{
email: "brenda@brenda.com"
},
{
email: "pam@pam.com"
}
]
))
})
it("returns a list of users from an instance db", async () => {
await config.createUser("brenda@brenda.com", "brendas_password")
await config.createUser("pam@pam.com", "pam_password")
const res = await request
.get(`/api/users`)
.get(`/api/users/metadata`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
@ -34,7 +55,7 @@ describe("/users", () => {
config,
request,
method: "GET",
url: `/api/users`,
url: `/api/users/metadata`,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
@ -42,9 +63,21 @@ describe("/users", () => {
})
describe("create", () => {
beforeEach(() => {
workerRequests.getGlobalUsers.mockImplementationOnce(() => ([
{
email: "bill@budibase.com"
},
{
email: "brandNewUser@user.com"
}
]
))
})
async function create(user, status = 200) {
return request
.post(`/api/users`)
.post(`/api/users/metadata`)
.set(config.defaultHeaders())
.send(user)
.expect(status)
@ -56,8 +89,8 @@ describe("/users", () => {
body.email = "bill@budibase.com"
const res = await create(body)
expect(res.res.statusMessage).toEqual("User created successfully.")
expect(res.body._id).toBeUndefined()
expect(res.res.statusMessage).toEqual("OK")
expect(res.body._id).toBeDefined()
})
it("should apply authorization to endpoint", async () => {
@ -67,18 +100,12 @@ describe("/users", () => {
config,
method: "POST",
body,
url: `/api/users`,
url: `/api/users/metadata`,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
it("should error if no email provided", async () => {
const user = basicUser(BUILTIN_ROLE_IDS.POWER)
delete user.email
await create(user, 400)
})
it("should error if no role provided", async () => {
const user = basicUser(null)
await create(user, 400)
@ -88,16 +115,22 @@ describe("/users", () => {
await config.createUser("test@test.com")
const user = basicUser(BUILTIN_ROLE_IDS.POWER)
user.email = "test@test.com"
await create(user, 400)
await create(user, 409)
})
})
describe("update", () => {
beforeEach(() => {
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
_id: "us_test@test.com"
}))
})
it("should be able to update the user", async () => {
const user = await config.createUser()
user.roleId = BUILTIN_ROLE_IDS.BASIC
const res = await request
.put(`/api/users`)
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send(user)
.expect(200)
@ -111,20 +144,29 @@ describe("/users", () => {
const email = "test@test.com"
await config.createUser(email)
const res = await request
.delete(`/api/users/${email}`)
.delete(`/api/users/metadata/${email}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toBeDefined()
expect(workerRequests.deleteGlobalUser).toHaveBeenCalled()
})
})
describe("find", () => {
beforeEach(() => {
jest.resetAllMocks()
workerRequests.getGlobalUsers.mockImplementationOnce(() => ({
email: "test@test.com",
roleId: BUILTIN_ROLE_IDS.POWER,
}))
})
it("should be able to find the user", async () => {
const email = "test@test.com"
await config.createUser(email)
const res = await request
.get(`/api/users/${email}`)
.get(`/api/users/metadata/${email}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)

View file

@ -3,7 +3,7 @@ const appController = require("../../../controllers/application")
const CouchDB = require("../../../../db")
function Request(appId, params) {
this.user = { appId }
this.appId = appId
this.params = params
}
@ -63,7 +63,11 @@ exports.checkPermissionsEndpoint = async ({
}) => {
const password = "PASSWORD"
await config.createUser("passUser@budibase.com", password, passRole)
const passHeader = await config.login("passUser@budibase.com", password)
const passHeader = await config.login(
"passUser@budibase.com",
password,
passRole
)
await exports
.createRequest(config.request, method, url, body)
@ -71,7 +75,11 @@ exports.checkPermissionsEndpoint = async ({
.expect(200)
await config.createUser("failUser@budibase.com", password, failRole)
const failHeader = await config.login("failUser@budibase.com", password)
const failHeader = await config.login(
"failUser@budibase.com",
password,
failRole
)
await exports
.createRequest(config.request, method, url, body)

View file

@ -11,31 +11,31 @@ const router = Router()
router
.get(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.fetch
controller.fetchMetadata
)
.get(
"/api/users/:email",
"/api/users/metadata/:email",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.find
controller.findMetadata
)
.put(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.update
controller.updateMetadata
)
.post(
"/api/users",
"/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.create
controller.createMetadata
)
.delete(
"/api/users/:email",
"/api/users/metadata/:email",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.destroy
controller.destroyMetadata
)
module.exports = router

View file

@ -1,3 +1,7 @@
// need to load environment first
const env = require("./environment")
const CouchDB = require("./db")
require("@budibase/auth").init(CouchDB)
const Koa = require("koa")
const destroyable = require("server-destroy")
const electron = require("electron")
@ -5,7 +9,6 @@ const koaBody = require("koa-body")
const logger = require("koa-pino-logger")
const http = require("http")
const api = require("./api")
const env = require("./environment")
const eventEmitter = require("./events")
const automations = require("./automations/index")
const Sentry = require("@sentry/node")

View file

@ -75,7 +75,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
request: {
body: inputs.row,
},
user: { appId },
appId,
eventEmitter: emitter,
}

View file

@ -61,9 +61,7 @@ module.exports.definition = {
module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
const { email, password, roleId } = inputs
const ctx = {
user: {
appId: appId,
},
appId,
request: {
body: { email, password, roleId },
},
@ -74,11 +72,11 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
if (env.isProd()) {
await usage.update(apiKey, usage.Properties.USER, 1)
}
await userController.create(ctx)
await userController.createMetadata(ctx)
return {
response: ctx.body,
// internal property not returned through the API
id: ctx.userId,
id: ctx.body._id,
revision: ctx.body._rev,
success: ctx.status === 200,
}

View file

@ -65,7 +65,7 @@ module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
rowId: inputs.id,
revId: inputs.revision,
},
user: { appId },
appId,
eventEmitter: emitter,
}

View file

@ -78,7 +78,7 @@ module.exports.run = async function({ inputs, appId, emitter }) {
request: {
body: inputs.row,
},
user: { appId },
appId,
eventEmitter: emitter,
}

View file

@ -1,10 +1,10 @@
require("../../environment")
const automation = require("../index")
const usageQuota = require("../../utilities/usageQuota")
const thread = require("../thread")
const triggers = require("../triggers")
const { basicAutomation, basicTable } = require("../../tests/utilities/structures")
const { wait } = require("../../utilities")
const env = require("../../environment")
const { makePartial } = require("../../tests/utilities")
const { cleanInputValues } = require("../automationUtils")
const setup = require("./utilities")

View file

@ -26,6 +26,7 @@ describe("test the create row action", () => {
})
expect(res.id).toBeDefined()
expect(res.revision).toBeDefined()
expect(res.success).toEqual(true)
const gottenRow = await config.getRow(table._id, res.id)
expect(gottenRow.name).toEqual("test")
expect(gottenRow.description).toEqual("test")

View file

@ -1,8 +1,7 @@
const usageQuota = require("../../utilities/usageQuota")
const env = require("../../environment")
const setup = require("./utilities")
const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
const { ViewNames } = require("../../db/utils")
const { InternalTables } = require("../../db/utils")
jest.mock("../../utilities/usageQuota")
@ -25,8 +24,7 @@ describe("test the create user action", () => {
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user)
expect(res.id).toBeDefined()
expect(res.revision).toBeDefined()
const userDoc = await config.getRow(ViewNames.USERS, res.id)
expect(userDoc.email).toEqual(user.email)
const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id)
})
it("should return an error if no inputs provided", async () => {

View file

@ -1,4 +1,5 @@
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const { UserStatus } = require("@budibase/auth")
exports.LOGO_URL =
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
@ -27,11 +28,6 @@ exports.AuthTypes = {
EXTERNAL: "external",
}
exports.UserStatus = {
ACTIVE: "active",
INACTIVE: "inactive",
}
exports.USERS_TABLE_SCHEMA = {
_id: "ta_users",
type: "table",
@ -68,7 +64,7 @@ exports.USERS_TABLE_SCHEMA = {
constraints: {
type: exports.FieldTypes.STRING,
presence: false,
inclusion: Object.values(exports.UserStatus),
inclusion: Object.values(UserStatus),
},
},
},

View file

@ -34,7 +34,10 @@ const DocumentTypes = {
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
USERS: "ta_users",
}
const InternalTables = {
USER_METADATA: "ta_users",
}
const SearchIndexes = {
@ -43,6 +46,7 @@ const SearchIndexes = {
exports.StaticDatabases = StaticDatabases
exports.ViewNames = ViewNames
exports.InternalTables = InternalTables
exports.DocumentTypes = DocumentTypes
exports.SEPARATOR = SEPARATOR
exports.UNICODE_MAX = UNICODE_MAX
@ -112,17 +116,19 @@ exports.getRowParams = (tableId = null, rowId = null, otherProps = {}) => {
/**
* Gets a new row ID for the specified table.
* @param {string} tableId The table which the row is being created for.
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
* @returns {string} The new ID which a row doc can be stored under.
*/
exports.generateRowID = tableId => {
return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${newid()}`
exports.generateRowID = (tableId, id = null) => {
id = id || newid()
return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}`
}
/**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
exports.getUserParams = (email = "", otherProps = {}) => {
return exports.getRowParams(ViewNames.USERS, email, otherProps)
exports.getUserMetadataParams = (email = "", otherProps = {}) => {
return exports.getRowParams(InternalTables.USER_METADATA, email, otherProps)
}
/**
@ -130,8 +136,17 @@ exports.getUserParams = (email = "", otherProps = {}) => {
* @param {string} email The email which the ID is going to be built up of.
* @returns {string} The new user ID which the user doc can be stored under.
*/
exports.generateUserID = email => {
return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}`
exports.generateUserMetadataID = email => {
return exports.generateRowID(InternalTables.USER_METADATA, email)
}
/**
* Breaks up the ID to get the email address back out of it.
*/
exports.getEmailFromUserMetadataID = id => {
return id.split(
`${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
)[1]
}
/**

View file

@ -1,73 +0,0 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const { getRole, getBuiltinRoles } = require("../utilities/security/roles")
const { AuthTypes } = require("../constants")
const {
getAppId,
getCookieName,
clearCookie,
setCookie,
isClient,
} = require("../utilities")
module.exports = async (ctx, next) => {
if (ctx.path === "/builder") {
await next()
return
}
// do everything we can to make sure the appId is held correctly
// we hold it in state as a
let appId = getAppId(ctx)
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
const builtinRoles = getBuiltinRoles()
if (appId && cookieAppId !== appId) {
setCookie(ctx, appId, "currentapp")
} else if (cookieAppId) {
appId = cookieAppId
}
let token, authType
if (!isClient(ctx)) {
token = ctx.cookies.get(getCookieName())
authType = AuthTypes.BUILDER
}
if (!token && appId) {
token = ctx.cookies.get(getCookieName(appId))
authType = AuthTypes.APP
}
if (!token) {
ctx.auth.authenticated = false
ctx.appId = appId
ctx.user = {
appId,
role: builtinRoles.PUBLIC,
}
await next()
return
}
try {
ctx.auth.authenticated = authType
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.appId = appId
ctx.auth.apiKey = jwtPayload.apiKey
ctx.user = {
...jwtPayload,
appId: appId,
role: await getRole(appId, jwtPayload.roleId),
}
} catch (err) {
console.log(err)
if (authType === AuthTypes.BUILDER) {
clearCookie(ctx)
ctx.status = 200
return
} else {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
}
}
await next()
}

View file

@ -1,58 +1,41 @@
const {
BUILTIN_ROLE_IDS,
getUserPermissions,
} = require("../utilities/security/roles")
const { getUserPermissions } = require("../utilities/security/roles")
const {
PermissionTypes,
doesHaveResourcePermission,
doesHaveBasePermission,
} = require("../utilities/security/permissions")
const env = require("../environment")
const { isAPIKeyValid } = require("../utilities/security/apikey")
const { AuthTypes } = require("../constants")
const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER]
function hasResource(ctx) {
return ctx.resourceId != null
}
module.exports = (permType, permLevel = null) => async (ctx, next) => {
if (env.isProd() && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) {
// api key header passed by external webhook
if (await isAPIKeyValid(ctx.headers["x-api-key"])) {
ctx.auth = {
authenticated: AuthTypes.EXTERNAL,
apiKey: ctx.headers["x-api-key"],
}
ctx.user = {
appId: ctx.headers["x-instanceid"],
}
return next()
}
const WEBHOOK_ENDPOINTS = new RegExp(
["webhooks/trigger", "webhooks/schema"].join("|")
)
return ctx.throw(403, "API key invalid")
module.exports = (permType, permLevel = null) => async (ctx, next) => {
// webhooks don't need authentication, each webhook unique
if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) {
return next()
}
if (!ctx.user) {
return ctx.throw(403, "No user info found")
}
const role = ctx.user.role
const isAdmin = ADMIN_ROLES.includes(role._id)
const isAuthed = ctx.auth.authenticated
const isAuthed = ctx.isAuthenticated
const { basePermissions, permissions } = await getUserPermissions(
ctx.appId,
role._id
ctx.roleId
)
// this may need to change in the future, right now only admins
// can have access to builder features, this is hard coded into
// our rules
if (isAdmin && isAuthed) {
// builders for now have permission to do anything
// TODO: in future should consider separating permissions with an require("@budibase/auth").isClient check
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
if (isBuilder) {
return next()
} else if (permType === PermissionTypes.BUILDER) {
} else if (permType === PermissionTypes.BUILDER && !isBuilder) {
return ctx.throw(403, "Not Authorized")
}

View file

@ -0,0 +1,59 @@
const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth")
const { getRole } = require("../utilities/security/roles")
const { generateUserMetadataID } = require("../db/utils")
const { getGlobalUsers } = require("../utilities/workerRequests")
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
module.exports = async (ctx, next) => {
// try to get the appID from the request
const requestAppId = getAppId(ctx)
// get app cookie if it exists
const appCookie = getCookie(ctx, Cookies.CurrentApp)
if (!appCookie && !requestAppId) {
return next()
}
let updateCookie = false,
appId,
roleId = BUILTIN_ROLE_IDS.PUBLIC
if (!ctx.user) {
// not logged in, try to set a cookie for public apps
updateCookie = true
appId = requestAppId
} else if (
requestAppId != null &&
(appCookie == null ||
requestAppId !== appCookie.appId ||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
) {
// Different App ID means cookie needs reset, or if the same public user has logged in
const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user.email)
updateCookie = true
appId = requestAppId
if (globalUser.roles && globalUser.roles[requestAppId]) {
roleId = globalUser.roles[requestAppId]
}
} else if (appCookie != null) {
appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
}
if (appId) {
ctx.appId = appId
if (roleId) {
const userId = ctx.user
? generateUserMetadataID(ctx.user.email)
: undefined
ctx.roleId = roleId
ctx.user = {
...ctx.user,
_id: userId,
userId,
role: await getRole(appId, roleId),
}
}
}
if (updateCookie && appId) {
setCookie(ctx, { appId, roleId }, Cookies.CurrentApp)
}
return next()
}

View file

@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = `
Object {
"apiKey": "1234",
"appId": "budibase:app:local",
"role": Role {
"_id": "ADMIN",
"inherits": "POWER",
"name": "Admin",
"permissionId": "admin",
},
"roleId": "ADMIN",
}
`;
exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = `
Object {
"apiKey": "1234",
"appId": "budibase:builder:local",
"role": Role {
"_id": "BUILDER",
"name": "Builder",
"permissionId": "admin",
},
"roleId": "BUILDER",
}
`;

View file

@ -1,124 +0,0 @@
const { AuthTypes } = require("../../constants")
const authenticatedMiddleware = require("../authenticated")
const jwt = require("jsonwebtoken")
jest.mock("jsonwebtoken")
class TestConfiguration {
constructor(middleware) {
this.middleware = authenticatedMiddleware
this.ctx = {
config: {},
auth: {},
cookies: {
set: jest.fn(),
get: jest.fn(),
},
headers: {},
params: {},
path: "",
request: {
headers: {},
},
throw: jest.fn(),
}
this.next = jest.fn()
}
setHeaders(headers) {
this.ctx.headers = headers
}
executeMiddleware() {
return this.middleware(this.ctx, this.next)
}
afterEach() {
jest.resetAllMocks()
}
}
describe("Authenticated middleware", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
config.afterEach()
})
it("calls next() when on the builder path", async () => {
config.ctx.path = "/builder"
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
it("sets a new cookie when the current cookie does not match the app id from context", async () => {
const appId = "app_123"
config.setHeaders({
"x-budibase-app-id": appId,
})
config.ctx.cookies.get.mockImplementation(() => "cookieAppId")
await config.executeMiddleware()
expect(config.ctx.cookies.set).toHaveBeenCalledWith(
"budibase:currentapp:local",
appId,
expect.any(Object)
)
})
it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => {
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
jwt.verify.mockImplementationOnce(() => ({
apiKey: "1234",
roleId: "BUILDER",
}))
await config.executeMiddleware()
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER)
expect(config.ctx.user).toMatchSnapshot()
})
it("sets the correct APP auth type information when the user is not in the builder", async () => {
config.setHeaders({
"x-budibase-type": "client",
})
config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`)
jwt.verify.mockImplementationOnce(() => ({
apiKey: "1234",
roleId: "ADMIN",
}))
await config.executeMiddleware()
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP)
expect(config.ctx.user).toMatchSnapshot()
})
it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => {
config.executeMiddleware()
expect(config.ctx.auth.authenticated).toBe(false)
expect(config.ctx.user.role).toEqual({
_id: "PUBLIC",
name: "Public",
permissionId: "public",
})
})
it("clears the cookie when there is an error authenticating in the builder", async () => {
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
jwt.verify.mockImplementationOnce(() => {
throw new Error()
})
await config.executeMiddleware()
expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local")
})
})

View file

@ -1,7 +1,5 @@
const authorizedMiddleware = require("../authorized")
const env = require("../../environment")
const apiKey = require("../../utilities/security/apikey")
const { AuthTypes } = require("../../constants")
const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions")
jest.mock("../../environment", () => ({
prod: false,
@ -12,7 +10,6 @@ jest.mock("../../environment", () => ({
}
})
)
jest.mock("../../utilities/security/apikey")
class TestConfiguration {
constructor(role) {
@ -92,29 +89,6 @@ describe("Authorization middleware", () => {
"x-instanceid": "instance123",
})
})
it("passes to next() if api key is valid", async () => {
apiKey.isAPIKeyValid.mockResolvedValueOnce(true)
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
expect(config.ctx.auth).toEqual({
authenticated: AuthTypes.EXTERNAL,
apiKey: config.ctx.headers["x-api-key"],
})
expect(config.ctx.user).toEqual({
appId: config.ctx.headers["x-instanceid"],
})
})
it("throws if api key is invalid", async () => {
apiKey.isAPIKeyValid.mockResolvedValueOnce(false)
await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith(403, "API key invalid")
})
})
describe("non-webhook call", () => {

View file

@ -0,0 +1,149 @@
mockAuthWithNoCookie()
mockWorker()
function mockWorker() {
jest.mock("../../utilities/workerRequests", () => ({
getGlobalUsers: () => {
return {
email: "test@test.com",
roles: {
"app_test": "BASIC",
}
}
}
}))
}
function mockReset() {
jest.resetModules()
mockWorker()
}
function mockAuthWithNoCookie() {
jest.resetModules()
mockWorker()
jest.mock("@budibase/auth", () => ({
getAppId: jest.fn(),
setCookie: jest.fn(),
getCookie: jest.fn(),
Cookies: {},
}))
}
function mockAuthWithCookie() {
jest.resetModules()
mockWorker()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: () => ({ appId: "app_different", roleId: "PUBLIC" }),
Cookies: {
Auth: "auth",
CurrentApp: "currentapp",
}
}))
}
class TestConfiguration {
constructor() {
this.next = jest.fn()
this.throw = jest.fn()
this.ctx = {
next: this.next,
throw: this.throw
}
}
setUser() {
this.ctx.user = {
email: "test@test.com",
}
}
executeMiddleware() {
// import as late as possible for mocks
const currentAppMiddleware = require("../currentapp")
return currentAppMiddleware(this.ctx, this.next)
}
}
describe("Current app middleware", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("test having no cookies or app ID", () => {
it("should be able to proceed with nothing setup", async () => {
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
})
describe("check get public for app when not logged in", () => {
it("should be able to proceed with no login, but cookies configured", async () => {
mockAuthWithCookie()
await config.executeMiddleware()
expect(config.ctx.roleId).toEqual("PUBLIC")
expect(config.ctx.appId).toEqual("app_test")
expect(config.next).toHaveBeenCalled()
})
})
describe("check functionality when logged in", () => {
async function checkExpected(setCookie) {
config.setUser()
await config.executeMiddleware()
const cookieFn = require("@budibase/auth").setCookie
if (setCookie) {
expect(cookieFn).toHaveBeenCalled()
} else {
expect(cookieFn).not.toHaveBeenCalled()
}
expect(config.ctx.roleId).toEqual("BASIC")
expect(config.ctx.user.role._id).toEqual("BASIC")
expect(config.ctx.appId).toEqual("app_test")
expect(config.next).toHaveBeenCalled()
}
it("should be able to setup an app token when cookie not setup", async () => {
mockAuthWithCookie()
await checkExpected(true)
})
it("should perform correct when no cookie exists", async () => {
mockReset()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: jest.fn(),
Cookies: {},
}))
await checkExpected(true)
})
it("lastly check what occurs when cookie doesn't need updated", async () => {
mockReset()
jest.mock("@budibase/auth", () => ({
getAppId: () => {
return "app_test"
},
setCookie: jest.fn(),
getCookie: () => ({ appId: "app_test", roleId: "BASIC" }),
Cookies: {},
}))
await checkExpected(false)
})
})
})

View file

@ -20,9 +20,7 @@ class TestConfiguration {
this.ctx = {
throw: this.throw,
next: this.next,
user: {
appId: "test"
},
appId: "test",
request: {
body: {}
},
@ -78,8 +76,10 @@ describe("usageQuota middleware", () => {
})
it("passes through to next middleware if document already exists", async () => {
config.setProd(true)
config.setBody({
_id: "test"
_id: "test",
_rev: "test",
})
CouchDB.mockImplementationOnce(() => ({
@ -89,13 +89,14 @@ describe("usageQuota middleware", () => {
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
expect(config.ctx.preExisting).toBe(true)
})
it("throws if request has _id, but the document no longer exists", async () => {
config.setBody({
_id: "123"
_id: "123",
_rev: "test",
})
config.setProd(true)
CouchDB.mockImplementationOnce(() => ({
get: async () => {

View file

@ -27,27 +27,27 @@ function getProperty(url) {
}
module.exports = async (ctx, next) => {
const db = new CouchDB(ctx.user.appId)
// if in development or a self hosted cloud usage quotas should not be executed
if (env.isDev() || env.SELF_HOSTED) {
return next()
}
const db = new CouchDB(ctx.appId)
let usage = METHOD_MAP[ctx.req.method]
const property = getProperty(ctx.req.url)
if (usage == null || property == null) {
return next()
}
// post request could be a save of a pre-existing entry
if (ctx.request.body && ctx.request.body._id) {
if (ctx.request.body && ctx.request.body._id && ctx.request.body._rev) {
try {
ctx.preExisting = await db.get(ctx.request.body._id)
await db.get(ctx.request.body._id)
return next()
} catch (err) {
ctx.throw(404, `${ctx.request.body._id} does not exist`)
return
}
}
// if in development or a self hosted cloud usage quotas should not be executed
if (env.isDev() || env.SELF_HOSTED) {
return next()
}
// update usage for uploads to be the total size
if (property === usageQuota.Properties.UPLOAD) {
const files =

View file

@ -15,6 +15,7 @@ const {
const controllers = require("./controllers")
const supertest = require("supertest")
const { cleanup } = require("../../utilities/fileSystem")
const { Cookies } = require("@budibase/auth")
const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password"
@ -68,16 +69,25 @@ class TestConfiguration {
}
defaultHeaders() {
const builderUser = {
userId: "BUILDER",
roleId: BUILTIN_ROLE_IDS.BUILDER,
const user = {
userId: "us_test@test.com",
email: "test@test.com",
builder: {
global: true,
},
}
const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
// can be "production" for test case
const type = env.isProd() ? "cloud" : "local"
const app = {
roleId: BUILTIN_ROLE_IDS.BUILDER,
appId: this.appId,
}
const authToken = jwt.sign(user, env.JWT_SECRET)
const appToken = jwt.sign(app, env.JWT_SECRET)
const headers = {
Accept: "application/json",
Cookie: [`budibase:builder:${type}=${builderToken}`],
Cookie: [
`${Cookies.Auth}=${authToken}`,
`${Cookies.CurrentApp}=${appToken}`,
],
}
if (this.appId) {
headers["x-budibase-app-id"] = this.appId
@ -101,7 +111,7 @@ class TestConfiguration {
} catch (err) {
// allow errors here
}
return this.login(email, PASSWORD)
return this.login(email, PASSWORD, roleId)
}
async createApp(appName) {
@ -279,7 +289,7 @@ class TestConfiguration {
roleId,
},
null,
controllers.user.create
controllers.user.createMetadata
)
}
@ -289,7 +299,7 @@ class TestConfiguration {
{
email,
},
controllers.user.find
controllers.user.findMetadata
)
return this._req(
{
@ -297,30 +307,35 @@ class TestConfiguration {
status: "inactive",
},
null,
controllers.user.update
controllers.user.updateMetadata
)
}
async login(email, password) {
async login(email, password, roleId = BUILTIN_ROLE_IDS.BUILDER) {
if (!this.request) {
throw "Server has not been opened, cannot login."
}
if (!email || !password) {
await this.createUser()
email = EMAIL
password = PASSWORD
}
const result = await this.request
.post(`/api/authenticate`)
.set({
"x-budibase-app-id": this.appId,
})
.send({ email, password })
const user = {
userId: `us_${email || EMAIL}`,
email: email || EMAIL,
}
const app = {
roleId: roleId,
appId: this.appId,
}
const authToken = jwt.sign(user, env.JWT_SECRET)
const appToken = jwt.sign(app, env.JWT_SECRET)
// returning necessary request headers
return {
Accept: "application/json",
Cookie: result.headers["set-cookie"],
Cookie: [
`${Cookies.Auth}=${authToken}`,
`${Cookies.CurrentApp}=${appToken}`,
],
"x-budibase-app-id": this.appId,
}
}

View file

@ -1,3 +1,5 @@
// TODO: REMOVE
const bcrypt = require("bcryptjs")
const env = require("../environment")

Some files were not shown because too many files have changed in this diff Show more