diff --git a/packages/worker/package.json b/packages/worker/package.json index 43deb14a50..73b417c580 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -21,6 +21,7 @@ "dependencies": { "@koa/router": "^8.0.0", "aws-sdk": "^2.811.0", + "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", "got": "^11.8.1", "joi": "^17.2.1", @@ -33,7 +34,7 @@ "koa-static": "^5.0.0", "node-fetch": "^2.6.1", "pino-pretty": "^4.0.0", - "pouchdb": "^7.2.2", + "pouchdb": "^7.2.1", "pouchdb-all-dbs": "^1.0.2", "server-destroy": "^1.0.1" }, diff --git a/packages/worker/src/api/controllers/admin/index.js b/packages/worker/src/api/controllers/admin/index.js new file mode 100644 index 0000000000..f94cf39fc4 --- /dev/null +++ b/packages/worker/src/api/controllers/admin/index.js @@ -0,0 +1,79 @@ +const CouchDB = require("../../../db") +const { StaticDatabases, generateUserID, getUserParams } = require("../../../db/utils") +const { hash } = require("./utils") +const { UserStatus } = require("../../../constants") + +const USER_DB = StaticDatabases.USER.name + +exports.userSave = async ctx => { + const db = new CouchDB(USER_DB) + const { email, password, _id } = ctx.request.body + const hashedPassword = password ? await hash(password) : null + let user = { + ...ctx.request.body, + _id: generateUserID(email), + password: hashedPassword, + }, dbUser + // in-case user existed already + if (_id) { + dbUser = await db.get(_id) + } + // add the active status to a user if its not provided + if (user.status == null) { + user.status = UserStatus.ACTIVE + } + try { + const response = await db.post({ + password: hashedPassword || dbUser.password, + ...user, + }) + ctx.body = { + _id: response.id, + _rev: response.rev, + email, + } + } catch (err) { + if (err.status === 409) { + ctx.throw(400, "User exists already") + } else { + ctx.throw(err.status, err) + } + } +} + +exports.userDelete = async ctx => { + const db = new CouchDB(USER_DB) + await db.destroy(generateUserID(ctx.params.email)) + ctx.body = { + message: `User ${ctx.params.email} deleted.`, + } +} + +// called internally by app server user fetch +exports.userFetch = async ctx => { + const db = new CouchDB(USER_DB) + const users = ( + await db.allDocs( + getUserParams(null, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc) + // user hashed password shouldn't ever be returned + for (let user of users) { + if (user) { + delete user.password + } + } + ctx.body = users +} + +// called internally by app server user find +exports.userFind = async ctx => { + const db = new CouchDB(USER_DB) + const user = await db.get(generateUserID(ctx.params.email)) + if (user) { + delete user.password + } + ctx.body = user +} diff --git a/packages/worker/src/api/controllers/admin/utils.js b/packages/worker/src/api/controllers/admin/utils.js new file mode 100644 index 0000000000..4af0a52c46 --- /dev/null +++ b/packages/worker/src/api/controllers/admin/utils.js @@ -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) +} \ No newline at end of file diff --git a/packages/worker/src/api/controllers/deploy.js b/packages/worker/src/api/controllers/deploy.js deleted file mode 100644 index cbaf842083..0000000000 --- a/packages/worker/src/api/controllers/deploy.js +++ /dev/null @@ -1,92 +0,0 @@ -const env = require("../../environment") -const got = require("got") -const AWS = require("aws-sdk") - -const APP_BUCKET = "app-assets" -// this doesn't matter in self host -const REGION = "eu-west-1" -const PUBLIC_READ_POLICY = { - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - AWS: ["*"], - }, - Action: "s3:GetObject", - Resource: [`arn:aws:s3:::${APP_BUCKET}/*`], - }, - ], -} - -async function getCouchSession() { - // fetch session token for the api user - const session = await got.post(`${env.COUCH_DB_URL}/_session`, { - responseType: "json", - credentials: "include", - json: { - username: env.COUCH_DB_USERNAME, - password: env.COUCH_DB_PASSWORD, - }, - }) - - const cookie = session.headers["set-cookie"][0] - // Get the session cookie value only - return cookie.split(";")[0] -} - -async function getMinioSession() { - AWS.config.update({ - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - }) - - // make sure the bucket exists - const objClient = new AWS.S3({ - endpoint: env.MINIO_URL, - region: REGION, - s3ForcePathStyle: true, // needed with minio? - params: { - Bucket: APP_BUCKET, - }, - }) - // make sure the bucket exists - try { - await objClient - .headBucket({ - Bucket: APP_BUCKET, - }) - .promise() - } catch (err) { - // bucket doesn't exist create it - if (err.statusCode === 404) { - await objClient - .createBucket({ - Bucket: APP_BUCKET, - }) - .promise() - } else { - throw err - } - } - // always make sure policy is correct - await objClient - .putBucketPolicy({ - Bucket: APP_BUCKET, - Policy: JSON.stringify(PUBLIC_READ_POLICY), - }) - .promise() - // Ideally want to send back some pre-signed URLs for files that are to be uploaded - return { - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - } -} - -exports.deploy = async ctx => { - ctx.body = { - couchDbSession: await getCouchSession(), - bucket: APP_BUCKET, - objectStoreSession: await getMinioSession(), - } -} diff --git a/packages/worker/src/api/routes/admin/index.js b/packages/worker/src/api/routes/admin/index.js new file mode 100644 index 0000000000..9d85927900 --- /dev/null +++ b/packages/worker/src/api/routes/admin/index.js @@ -0,0 +1,12 @@ +const Router = require("@koa/router") +const controller = require("../../controllers/admin") +const authorized = require("../../../middleware/authorized") + +const router = Router() + +router.post("/api/admin/users", authorized, controller.userSave) + .delete("/api/admin/users/:email", authorized, controller.userDelete) + .get("/api/admin/users", authorized, controller.userFetch) + .get("/api/admin/users/:email", authorized, controller.userFind) + +module.exports = router diff --git a/packages/worker/src/api/routes/deploy.js b/packages/worker/src/api/routes/deploy.js deleted file mode 100644 index 97c5439752..0000000000 --- a/packages/worker/src/api/routes/deploy.js +++ /dev/null @@ -1,9 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../controllers/deploy") -const checkKey = require("../../middleware/check-key") - -const router = Router() - -router.post("/api/deploy", checkKey, controller.deploy) - -module.exports = router diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index 7ad619c2b7..076710b21b 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -1,4 +1,4 @@ -const deployRoutes = require("./deploy") +const adminRoutes = require("./admin") const appRoutes = require("./app") -exports.routes = [deployRoutes, appRoutes] +exports.routes = [adminRoutes, appRoutes] diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js new file mode 100644 index 0000000000..77a6aedca0 --- /dev/null +++ b/packages/worker/src/constants/index.js @@ -0,0 +1,4 @@ +exports.UserStatus = { + ACTIVE: "active", + INACTIVE: "inactive", +} diff --git a/packages/worker/src/db/utils.js b/packages/worker/src/db/utils.js new file mode 100644 index 0000000000..aa274eff3b --- /dev/null +++ b/packages/worker/src/db/utils.js @@ -0,0 +1,32 @@ +exports.StaticDatabases = { + USER: { + name: "user-db", + } +} + +const DocumentTypes = { + USER: "us" +} + +const UNICODE_MAX = "\ufff0" +const 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}` +} + +/** + * Gets parameters for retrieving users, this is a utility function for the getDocParams function. + */ +exports.getUserParams = (email = "", otherProps = {}) => { + return { + ...otherProps, + startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`, + endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, + } +} \ No newline at end of file diff --git a/packages/worker/src/environment.js b/packages/worker/src/environment.js index b857dc6098..bf399fade5 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.js @@ -16,6 +16,8 @@ module.exports = { MINIO_URL: process.env.MINIO_URL, COUCH_DB_URL: process.env.COUCH_DB_URL, LOG_LEVEL: process.env.LOG_LEVEL, + JWT_SECRET: process.env.JWT_SECRET, + SALT_ROUNDS: process.env.SALT_ROUNDS, /* TODO: to remove - once deployment removed */ SELF_HOST_KEY: process.env.SELF_HOST_KEY, COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME, diff --git a/packages/worker/src/middleware/authorized.js b/packages/worker/src/middleware/authorized.js new file mode 100644 index 0000000000..86a5ab27db --- /dev/null +++ b/packages/worker/src/middleware/authorized.js @@ -0,0 +1,7 @@ +/** + * Check the user token, used when creating admin resources, like for example + * a global user record. + */ +module.exports = async (ctx, next) => { + next() +} diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 70bea3b7ea..d969c67ceb 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -285,6 +285,11 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1813,7 +1818,7 @@ pouchdb-promise@5.4.3: dependencies: lie "3.0.4" -pouchdb@^7.2.2: +pouchdb@^7.2.1: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-7.2.2.tgz#fcae82862db527e4cf7576ed8549d1384961f364" integrity sha512-5gf5nw5XH/2H/DJj8b0YkvG9fhA/4Jt6kL0Y8QjtztVjb1y4J19Rg4rG+fUbXu96gsUrlyIvZ3XfM0b4mogGmw==