Merge pull request #1392 from Budibase/feature/user-id-update

Replacing User IDs with Emails
This commit is contained in:
Michael Drury 2021-04-21 14:38:30 +01:00 committed by GitHub
commit 54150a541c
36 changed files with 334 additions and 367 deletions

View File

@ -1,5 +1,9 @@
let Pouch
module.exports.setDB = pouch => { module.exports.setDB = pouch => {
module.exports.CouchDB = pouch Pouch = pouch
} }
module.exports.CouchDB = null module.exports.getDB = dbName => {
return new Pouch(dbName)
}

View File

@ -1,5 +1,9 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
exports.ViewNames = {
USER_BY_EMAIL: "by_email",
}
exports.StaticDatabases = { exports.StaticDatabases = {
GLOBAL: { GLOBAL: {
name: "global-db", name: "global-db",
@ -19,19 +23,6 @@ const SEPARATOR = "_"
exports.SEPARATOR = 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]
}
/** /**
* Generates a new group ID. * Generates a new group ID.
* @returns {string} The new group ID which the group doc can be stored under. * @returns {string} The new group ID which the group doc can be stored under.
@ -51,16 +42,24 @@ exports.getGroupParams = (id = "", otherProps = {}) => {
} }
} }
/**
* Generates a new global user ID.
* @returns {string} The new user ID which the user doc can be stored under.
*/
exports.generateGlobalUserID = () => {
return `${DocumentTypes.USER}${SEPARATOR}${newid()}`
}
/** /**
* Gets parameters for retrieving users. * Gets parameters for retrieving users.
*/ */
exports.getUserParams = (email = "", otherProps = {}) => { exports.getGlobalUserParams = (globalId = "", otherProps = {}) => {
if (!email) { if (!globalId) {
email = "" globalId = ""
} }
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.USER}${SEPARATOR}${email}`, startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`,
endkey: `${DocumentTypes.USER}${SEPARATOR}${email}${UNICODE_MAX}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
} }
} }

View File

@ -0,0 +1,35 @@
const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils")
const { getDB } = require("./index")
function DesignDoc() {
return {
_id: "_design/database",
// view collation information, read before writing any complex views:
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
views: {},
}
}
exports.createUserEmailView = async () => {
const db = getDB(StaticDatabases.GLOBAL.name)
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentTypes.USER}")) {
emit(doc.email, doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewNames.USER_BY_EMAIL]: view,
}
await db.put(designDoc)
}

View File

@ -2,7 +2,7 @@ const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
// const GoogleStrategy = require("passport-google-oauth").Strategy // const GoogleStrategy = require("passport-google-oauth").Strategy
const database = require("./db") const { setDB, getDB } = require("./db")
const { StaticDatabases } = require("./db/utils") const { StaticDatabases } = require("./db/utils")
const { jwt, local, authenticated } = require("./middleware") const { jwt, local, authenticated } = require("./middleware")
const { Cookies, UserStatus } = require("./constants") const { Cookies, UserStatus } = require("./constants")
@ -13,13 +13,13 @@ const {
getCookie, getCookie,
clearCookie, clearCookie,
isClient, isClient,
getGlobalUserByEmail,
} = require("./utils") } = require("./utils")
const { const {
generateUserID, generateGlobalUserID,
getUserParams, getGlobalUserParams,
generateGroupID, generateGroupID,
getGroupParams, getGroupParams,
getEmailFromUserID,
} = require("./db/utils") } = require("./db/utils")
// Strategies // Strategies
@ -30,7 +30,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user)) passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser(async (user, done) => { passport.deserializeUser(async (user, done) => {
const db = new database.CouchDB(StaticDatabases.GLOBAL.name) const db = getDB(StaticDatabases.GLOBAL.name)
try { try {
const user = await db.get(user._id) const user = await db.get(user._id)
@ -43,17 +43,16 @@ passport.deserializeUser(async (user, done) => {
module.exports = { module.exports = {
init(pouch) { init(pouch) {
database.setDB(pouch) setDB(pouch)
}, },
passport, passport,
Cookies, Cookies,
UserStatus, UserStatus,
StaticDatabases, StaticDatabases,
generateUserID, generateGlobalUserID,
getUserParams, getGlobalUserParams,
generateGroupID, generateGroupID,
getGroupParams, getGroupParams,
getEmailFromUserID,
hash, hash,
compare, compare,
getAppId, getAppId,
@ -62,4 +61,5 @@ module.exports = {
clearCookie, clearCookie,
authenticated, authenticated,
isClient, isClient,
getGlobalUserByEmail,
} }

View File

@ -1,6 +1,5 @@
const { Cookies } = require("../constants") const { Cookies } = require("../constants")
const { getCookie } = require("../utils") const { getCookie } = require("../utils")
const { getEmailFromUserID } = require("../db/utils")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
try { try {
@ -10,8 +9,6 @@ module.exports = async (ctx, next) => {
if (authCookie) { if (authCookie) {
ctx.isAuthenticated = true ctx.isAuthenticated = true
ctx.user = authCookie ctx.user = authCookie
// make sure email is correct from ID
ctx.user.email = getEmailFromUserID(authCookie.userId)
} }
await next() await next()

View File

@ -1,9 +1,8 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { UserStatus } = require("../../constants") const { UserStatus } = require("../../constants")
const database = require("../../db")
const { StaticDatabases, generateUserID } = require("../../db/utils")
const { compare } = require("../../hashing") const { compare } = require("../../hashing")
const env = require("../../environment") const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid Credentials"
@ -11,23 +10,17 @@ exports.options = {}
/** /**
* Passport Local Authentication Middleware. * Passport Local Authentication Middleware.
* @param {*} username - username to login with * @param {*} email - username to login with
* @param {*} password - plain text password to log in with * @param {*} password - plain text password to log in with
* @param {*} done - callback from passport to return user information and errors * @param {*} done - callback from passport to return user information and errors
* @returns The authenticated user, or errors if they occur * @returns The authenticated user, or errors if they occur
*/ */
exports.authenticate = async function(username, password, done) { exports.authenticate = async function(email, password, done) {
if (!username) return done(null, false, "Email Required.") if (!email) return done(null, false, "Email Required.")
if (!password) return done(null, false, "Password Required.") if (!password) return done(null, false, "Password Required.")
// Check the user exists in the instance DB by email const dbUser = await getGlobalUserByEmail(email)
const db = new database.CouchDB(StaticDatabases.GLOBAL.name) if (dbUser == null) {
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" }) return done(null, false, { message: "User not found" })
} }

View File

@ -1,6 +1,13 @@
const { DocumentTypes, SEPARATOR } = require("./db/utils") const {
DocumentTypes,
SEPARATOR,
ViewNames,
StaticDatabases,
} = require("./db/utils")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views")
const { getDB } = require("./db")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -97,3 +104,24 @@ exports.clearCookie = (ctx, name) => {
exports.isClient = ctx => { exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client" return ctx.headers["x-budibase-type"] === "client"
} }
exports.getGlobalUserByEmail = async email => {
const db = getDB(StaticDatabases.GLOBAL.name)
try {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
key: email,
include_docs: true,
})
).rows
users = users.map(user => user.doc)
return users.length <= 1 ? users[0] : users
} catch (err) {
if (err != null && err.name === "not_found") {
await createUserEmailView()
return exports.getGlobalUserByEmail(email)
} else {
throw err
}
}
}

View File

@ -6,12 +6,8 @@ context("Create Bindings", () => {
}) })
it("should add a current user binding", () => { it("should add a current user binding", () => {
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.addComponent("Elements", "Paragraph").then(() => {
addSettingBinding("text", "Current User._id") addSettingBinding("text", "Current User._id")
cy.getComponent(componentId).should(
"have.text",
`ro_ta_users_test@test.com`
)
}) })
}) })

View File

@ -1,28 +1,9 @@
// *********************************************** // ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom // For more comprehensive examples of custom
// commands please read more here: // commands please read more here:
// https://on.cypress.io/custom-commands // https://on.cypress.io/custom-commands
// *********************************************** // ***********************************************
// //
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add("login", () => { Cypress.Commands.add("login", () => {
cy.getCookie("budibase:auth").then(cookie => { cy.getCookie("budibase:auth").then(cookie => {

View File

@ -14,9 +14,10 @@
"cy:setup": "node ./cypress/setup.js", "cy:setup": "node ./cypress/setup.js",
"cy:run": "cypress run", "cy:run": "cypress run",
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run:ci": "cypress run --browser electron --record --key f308590b-6070-41af-b970-794a3823d451", "cy:run:ci": "cypress run --record --key f308590b-6070-41af-b970-794a3823d451",
"cy:test": "start-server-and-test cy:setup http://localhost:10000/builder cy:run", "cy:test": "start-server-and-test cy:setup http://localhost:10000/builder cy:run",
"cy:ci": "start-server-and-test cy:setup http://localhost:10000/builder cy:run:ci" "cy:ci": "start-server-and-test cy:setup http://localhost:10000/builder cy:run:ci",
"cy:debug": "start-server-and-test cy:setup http://localhost:10000/builder cy:open"
}, },
"jest": { "jest": {
"globals": { "globals": {

View File

@ -21,14 +21,7 @@
async function createTestUser() { async function createTestUser() {
try { try {
await auth.createUser({ await auth.firstUser()
email: "test@test.com",
password: "test",
roles: {},
builder: {
global: true,
},
})
notifier.success("Test user created") notifier.success("Test user created")
} catch (err) { } catch (err) {
console.error(err) console.error(err)

View File

@ -151,8 +151,8 @@
const user = { const user = {
roleId: $createAppStore.values.roleId, roleId: $createAppStore.values.roleId,
} }
const userResp = await api.post(`/api/users/metadata`, user) const userResp = await api.post(`/api/users/metadata/self`, user)
const json = await userResp.json() await userResp.json()
$goto(`./${appJson._id}`) $goto(`./${appJson._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -30,11 +30,24 @@ export function createAuthStore() {
}, },
logout: async () => { logout: async () => {
const response = await api.post(`/api/admin/auth/logout`) const response = await api.post(`/api/admin/auth/logout`)
if (response.status !== 200) {
throw "Unable to create logout"
}
await response.json() await response.json()
set({ user: null }) set({ user: null })
}, },
createUser: async user => { createUser: async user => {
const response = await api.post(`/api/admin/users`, user) const response = await api.post(`/api/admin/users`, user)
if (response.status !== 200) {
throw "Unable to create user"
}
await response.json()
},
firstUser: async () => {
const response = await api.post(`/api/admin/users/first`)
if (response.status !== 200) {
throw "Unable to create test user"
}
await response.json() await response.json()
}, },
} }

View File

@ -1,75 +1,8 @@
const jwt = require("jsonwebtoken")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const env = require("../../environment")
const { getAPIKey } = require("../../utilities/usageQuota")
const { generateUserMetadataID } = require("../../db/utils")
const { setCookie } = require("../../utilities")
const { outputProcessing } = require("../../utilities/rowProcessor") const { outputProcessing } = require("../../utilities/rowProcessor")
const { InternalTables } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
const { UserStatus } = require("@budibase/auth")
const { getFullUser } = require("../../utilities/users") const { getFullUser } = require("../../utilities/users")
const INVALID_ERR = "Invalid Credentials"
exports.authenticate = async ctx => {
const appId = ctx.appId
if (!appId) ctx.throw(400, "No appId")
const { email, password } = ctx.request.body
if (!email) ctx.throw(400, "Email Required.")
if (!password) ctx.throw(400, "Password Required.")
// Check the user exists in the instance DB by email
const db = new CouchDB(appId)
const app = await db.get(appId)
let dbUser
try {
dbUser = await db.get(generateUserMetadataID(email))
} catch (_) {
// do not want to throw a 404 - as this could be
// used to determine valid emails
ctx.throw(401, INVALID_ERR)
}
// check that the user is currently inactive, if this is the case throw invalid
if (dbUser.status === UserStatus.INACTIVE) {
ctx.throw(401, INVALID_ERR)
}
// authenticate
if (await bcrypt.compare(password, dbUser.password)) {
const payload = {
userId: dbUser._id,
roleId: dbUser.roleId,
version: app.version,
}
// 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.appId)
payload.apiKey = apiKey
}
const token = jwt.sign(payload, ctx.config.jwtSecret, {
expiresIn: "1 day",
})
setCookie(ctx, token, appId)
delete dbUser.password
ctx.body = {
token,
...dbUser,
appId,
}
} else {
ctx.throw(401, INVALID_ERR)
}
}
exports.fetchSelf = async ctx => { exports.fetchSelf = async ctx => {
if (!ctx.user) { if (!ctx.user) {
ctx.throw(403, "No user logged in") ctx.throw(403, "No user logged in")
@ -82,7 +15,7 @@ exports.fetchSelf = async ctx => {
return return
} }
const user = await getFullUser({ ctx, userId: userId }) const user = await getFullUser(ctx, userId)
if (appId) { if (appId) {
const db = new CouchDB(appId) const db = new CouchDB(appId)

View File

@ -4,9 +4,9 @@ const { checkSlashesInUrl } = require("../../utilities")
const { request } = require("../../utilities/workerRequests") const { request } = require("../../utilities/workerRequests")
async function redirect(ctx, method) { async function redirect(ctx, method) {
const { path } = ctx.params const { devPath } = ctx.params
const response = await fetch( const response = await fetch(
checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${path}`), checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${devPath}`),
request(ctx, { request(ctx, {
method, method,
body: ctx.request.body, body: ctx.request.body,

View File

@ -7,7 +7,6 @@ const {
DocumentTypes, DocumentTypes,
SEPARATOR, SEPARATOR,
InternalTables, InternalTables,
generateUserMetadataID,
} = require("../../db/utils") } = require("../../db/utils")
const userController = require("./user") const userController = require("./user")
const { const {
@ -42,7 +41,7 @@ async function findRow(ctx, db, tableId, rowId) {
// TODO remove special user case in future // TODO remove special user case in future
if (tableId === InternalTables.USER_METADATA) { if (tableId === InternalTables.USER_METADATA) {
ctx.params = { ctx.params = {
userId: rowId, id: rowId,
} }
await userController.findMetadata(ctx) await userController.findMetadata(ctx)
row = ctx.body row = ctx.body
@ -140,12 +139,7 @@ exports.save = async function(ctx) {
} }
if (!inputs._rev && !inputs._id) { if (!inputs._rev && !inputs._id) {
// TODO remove special user case in future inputs._id = generateRowID(inputs.tableId)
if (inputs.tableId === InternalTables.USER_METADATA) {
inputs._id = generateUserMetadataID(inputs.email)
} else {
inputs._id = generateRowID(inputs.tableId)
}
} }
// this returns the table and row incase they have been updated // this returns the table and row incase they have been updated
@ -342,7 +336,7 @@ exports.destroy = async function(ctx) {
// TODO remove special user case in future // TODO remove special user case in future
if (ctx.params.tableId === InternalTables.USER_METADATA) { if (ctx.params.tableId === InternalTables.USER_METADATA) {
ctx.params = { ctx.params = {
userId: ctx.params.rowId, id: ctx.params.rowId,
} }
await userController.destroyMetadata(ctx) await userController.destroyMetadata(ctx)
} else { } else {
@ -449,7 +443,7 @@ async function bulkDelete(ctx) {
updates = updates.concat( updates = updates.concat(
rows.map(row => { rows.map(row => {
ctx.params = { ctx.params = {
userId: row._id, id: row._id,
} }
return userController.destroyMetadata(ctx) return userController.destroyMetadata(ctx)
}) })

View File

@ -2,7 +2,7 @@ const CouchDB = require("../../db")
const { const {
generateUserMetadataID, generateUserMetadataID,
getUserMetadataParams, getUserMetadataParams,
getEmailFromUserMetadataID, getGlobalIDFromUserMetadataID,
} = require("../../db/utils") } = require("../../db/utils")
const { InternalTables } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
const { getRole } = require("../../utilities/security/roles") const { getRole } = require("../../utilities/security/roles")
@ -25,15 +25,14 @@ exports.fetchMetadata = async function(ctx) {
).rows.map(row => row.doc) ).rows.map(row => row.doc)
const users = [] const users = []
for (let user of global) { for (let user of global) {
const info = metadata.find(meta => meta._id.includes(user.email)) // find the metadata that matches up to the global ID
const info = metadata.find(meta => meta._id.includes(user._id))
// remove these props, not for the correct DB // remove these props, not for the correct DB
delete user._id
delete user._rev
users.push({ users.push({
...user, ...user,
...info, ...info,
// make sure the ID is always a local ID, not a global one // make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(user.email), _id: generateUserMetadataID(user._id),
}) })
} }
ctx.body = users ctx.body = users
@ -43,17 +42,20 @@ exports.createMetadata = async function(ctx) {
const appId = ctx.appId const appId = ctx.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
const { roleId } = ctx.request.body const { roleId } = ctx.request.body
const email = ctx.request.body.email || ctx.user.email
if (ctx.request.body._id) {
return exports.updateMetadata(ctx)
}
// check role valid // check role valid
const role = await getRole(appId, roleId) const role = await getRole(appId, roleId)
if (!role) ctx.throw(400, "Invalid Role") if (!role) ctx.throw(400, "Invalid Role")
const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body) const globalUser = await saveGlobalUser(ctx, appId, ctx.request.body)
const user = { const user = {
...metadata, ...globalUser,
_id: generateUserMetadataID(email), _id: generateUserMetadataID(globalUser._id),
type: "user", type: "user",
tableId: InternalTables.USER_METADATA, tableId: InternalTables.USER_METADATA,
} }
@ -64,47 +66,48 @@ exports.createMetadata = async function(ctx) {
ctx.body = { ctx.body = {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
email, email: ctx.request.body.email,
} }
} }
exports.updateSelfMetadata = async function(ctx) {
// overwrite the ID with current users
ctx.request.body._id = ctx.user.userId
// make sure no stale rev
delete ctx.request.body._rev
await exports.updateMetadata(ctx)
}
exports.updateMetadata = async function(ctx) { exports.updateMetadata = async function(ctx) {
const appId = ctx.appId const appId = ctx.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
const user = ctx.request.body const user = ctx.request.body
let email = user.email || getEmailFromUserMetadataID(user._id) const globalUser = await saveGlobalUser(ctx, appId, {
const metadata = await saveGlobalUser(ctx, appId, email, ctx.request.body) ...user,
if (!metadata._id) { _id: getGlobalIDFromUserMetadataID(user._id),
metadata._id = generateUserMetadataID(email)
}
if (!metadata._rev) {
metadata._rev = ctx.request.body._rev
}
ctx.body = await db.put({
...metadata,
}) })
const metadata = {
...globalUser,
_id: user._id || generateUserMetadataID(globalUser._id),
_rev: user._rev,
}
ctx.body = await db.put(metadata)
} }
exports.destroyMetadata = async function(ctx) { exports.destroyMetadata = async function(ctx) {
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
const email = await deleteGlobalUser(ctx, getGlobalIDFromUserMetadataID(ctx.params.id))
ctx.params.email || getEmailFromUserMetadataID(ctx.params.userId)
await deleteGlobalUser(ctx, email)
try { try {
const dbUser = await db.get(generateUserMetadataID(email)) const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
} catch (err) { } catch (err) {
// error just means the global user has no config in this app // error just means the global user has no config in this app
} }
ctx.body = { ctx.body = {
message: `User ${ctx.params.email} deleted.`, message: `User ${ctx.params.id} deleted.`,
} }
} }
exports.findMetadata = async function(ctx) { exports.findMetadata = async function(ctx) {
ctx.body = await getFullUser({ ctx.body = await getFullUser(ctx, ctx.params.id)
ctx,
email: ctx.params.email,
userId: ctx.params.userId,
})
} }

View File

@ -5,9 +5,10 @@ const env = require("../../environment")
const router = Router() const router = Router()
if (env.isDev() || env.isTest()) { if (env.isDev() || env.isTest()) {
router.get("/api/admin/:path", controller.redirectGet) router
router.post("/api/admin/:path", controller.redirectPost) .get("/api/admin/:devPath(.*)", controller.redirectGet)
router.delete("/api/admin/:path", controller.redirectDelete) .post("/api/admin/:devPath(.*)", controller.redirectPost)
.delete("/api/admin/:devPath(.*)", controller.redirectDelete)
} }
module.exports = router module.exports = router

View File

@ -1,13 +1,18 @@
const setup = require("./utilities") const setup = require("./utilities")
const { generateUserMetadataID } = require("../../../db/utils")
require("../../../utilities/workerRequests") require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({ jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => { getGlobalUsers: jest.fn(() => {
return { return {
email: "test@test.com", _id: "us_uuid1",
}
}),
saveGlobalUser: jest.fn(() => {
return {
_id: "us_uuid1",
} }
}), }),
saveGlobalUser: jest.fn(),
})) }))
describe("/authenticate", () => { describe("/authenticate", () => {
@ -22,14 +27,14 @@ describe("/authenticate", () => {
describe("fetch self", () => { describe("fetch self", () => {
it("should be able to fetch self", async () => { it("should be able to fetch self", async () => {
await config.createUser("test@test.com", "p4ssw0rd") const user = await config.createUser("test@test.com", "p4ssw0rd")
const headers = await config.login("test@test.com", "p4ssw0rd") const headers = await config.login("test@test.com", "p4ssw0rd", { userId: "us_uuid1" })
const res = await request const res = await request
.get(`/api/self`) .get(`/api/self`)
.set(headers) .set(headers)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.email).toEqual("test@test.com") expect(res.body._id).toEqual(generateUserMetadataID("us_uuid1"))
}) })
}) })
}) })

View File

@ -4,11 +4,6 @@ const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const workerRequests = require("../../../utilities/workerRequests") const workerRequests = require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(),
saveGlobalUser: jest.fn(),
}))
const route = "/test" const route = "/test"
describe("/routing", () => { describe("/routing", () => {

View File

@ -7,7 +7,10 @@ const workerRequests = require("../../../utilities/workerRequests")
jest.mock("../../../utilities/workerRequests", () => ({ jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(), getGlobalUsers: jest.fn(),
saveGlobalUser: jest.fn(() => { saveGlobalUser: jest.fn(() => {
return {} const uuid = require("uuid/v4")
return {
_id: `us_${uuid()}`
}
}), }),
deleteGlobalUser: jest.fn(), deleteGlobalUser: jest.fn(),
})) }))
@ -26,10 +29,10 @@ describe("/users", () => {
beforeEach(() => { beforeEach(() => {
workerRequests.getGlobalUsers.mockImplementationOnce(() => ([ workerRequests.getGlobalUsers.mockImplementationOnce(() => ([
{ {
email: "brenda@brenda.com" _id: "us_uuid1",
}, },
{ {
email: "pam@pam.com" _id: "us_uuid2",
} }
] ]
)) ))
@ -45,8 +48,8 @@ describe("/users", () => {
.expect(200) .expect(200)
expect(res.body.length).toBe(2) expect(res.body.length).toBe(2)
expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined() expect(res.body.find(u => u._id === `ro_ta_users_us_uuid1`)).toBeDefined()
expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined() expect(res.body.find(u => u._id === `ro_ta_users_us_uuid2`)).toBeDefined()
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
@ -66,10 +69,10 @@ describe("/users", () => {
beforeEach(() => { beforeEach(() => {
workerRequests.getGlobalUsers.mockImplementationOnce(() => ([ workerRequests.getGlobalUsers.mockImplementationOnce(() => ([
{ {
email: "bill@budibase.com" _id: "us_uuid1",
}, },
{ {
email: "brandNewUser@user.com" _id: "us_uuid2",
} }
] ]
)) ))
@ -86,7 +89,6 @@ describe("/users", () => {
it("returns a success message when a user is successfully created", async () => { it("returns a success message when a user is successfully created", async () => {
const body = basicUser(BUILTIN_ROLE_IDS.POWER) const body = basicUser(BUILTIN_ROLE_IDS.POWER)
body.email = "bill@budibase.com"
const res = await create(body) const res = await create(body)
expect(res.res.statusMessage).toEqual("OK") expect(res.res.statusMessage).toEqual("OK")
@ -95,7 +97,6 @@ describe("/users", () => {
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
const body = basicUser(BUILTIN_ROLE_IDS.POWER) const body = basicUser(BUILTIN_ROLE_IDS.POWER)
body.email = "brandNewUser@user.com"
await checkPermissionsEndpoint({ await checkPermissionsEndpoint({
config, config,
method: "POST", method: "POST",
@ -110,13 +111,6 @@ describe("/users", () => {
const user = basicUser(null) const user = basicUser(null)
await create(user, 400) await create(user, 400)
}) })
it("should throw error if user exists already", async () => {
await config.createUser("test@test.com")
const user = basicUser(BUILTIN_ROLE_IDS.POWER)
user.email = "test@test.com"
await create(user, 409)
})
}) })
describe("update", () => { describe("update", () => {
@ -141,10 +135,9 @@ describe("/users", () => {
describe("destroy", () => { describe("destroy", () => {
it("should be able to delete the user", async () => { it("should be able to delete the user", async () => {
const email = "test@test.com" const user = await config.createUser()
await config.createUser(email)
const res = await request const res = await request
.delete(`/api/users/metadata/${email}`) .delete(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(200) .expect(200)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
@ -156,21 +149,23 @@ describe("/users", () => {
describe("find", () => { describe("find", () => {
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks() jest.resetAllMocks()
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
_id: "us_uuid1",
}))
workerRequests.getGlobalUsers.mockImplementationOnce(() => ({ workerRequests.getGlobalUsers.mockImplementationOnce(() => ({
email: "test@test.com", _id: "us_uuid1",
roleId: BUILTIN_ROLE_IDS.POWER, roleId: BUILTIN_ROLE_IDS.POWER,
})) }))
}) })
it("should be able to find the user", async () => { it("should be able to find the user", async () => {
const email = "test@test.com" const user = await config.createUser()
await config.createUser(email)
const res = await request const res = await request
.get(`/api/users/metadata/${email}`) .get(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(200) .expect(200)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
expect(res.body.email).toEqual(email) expect(res.body._id).toEqual(user._id)
expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.POWER) expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(res.body.tableId).toBeDefined() expect(res.body.tableId).toBeDefined()
}) })

View File

@ -63,11 +63,9 @@ exports.checkPermissionsEndpoint = async ({
}) => { }) => {
const password = "PASSWORD" const password = "PASSWORD"
await config.createUser("passUser@budibase.com", password, passRole) await config.createUser("passUser@budibase.com", password, passRole)
const passHeader = await config.login( const passHeader = await config.login("passUser@budibase.com", password, {
"passUser@budibase.com", roleId: passRole,
password, })
passRole
)
await exports await exports
.createRequest(config.request, method, url, body) .createRequest(config.request, method, url, body)
@ -75,11 +73,9 @@ exports.checkPermissionsEndpoint = async ({
.expect(200) .expect(200)
await config.createUser("failUser@budibase.com", password, failRole) await config.createUser("failUser@budibase.com", password, failRole)
const failHeader = await config.login( const failHeader = await config.login("failUser@budibase.com", password, {
"failUser@budibase.com", roleId: failRole,
password, })
failRole
)
await exports await exports
.createRequest(config.request, method, url, body) .createRequest(config.request, method, url, body)

View File

@ -2,6 +2,15 @@ const TestConfig = require("../../../../tests/utilities/TestConfiguration")
const structures = require("../../../../tests/utilities/structures") const structures = require("../../../../tests/utilities/structures")
const env = require("../../../../environment") const env = require("../../../../environment")
jest.mock("../../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(),
saveGlobalUser: jest.fn(() => {
return {
_id: "us_uuid1",
}
}),
}))
exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms)) exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms))
let request, config let request, config

View File

@ -16,7 +16,7 @@ router
controller.fetchMetadata controller.fetchMetadata
) )
.get( .get(
"/api/users/metadata/:email", "/api/users/metadata/:id",
authorized(PermissionTypes.USER, PermissionLevels.READ), authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.findMetadata controller.findMetadata
) )
@ -31,8 +31,14 @@ router
usage, usage,
controller.createMetadata controller.createMetadata
) )
.post(
"/api/users/metadata/self",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.updateSelfMetadata
)
.delete( .delete(
"/api/users/metadata/:email", "/api/users/metadata/:id",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage, usage,
controller.destroyMetadata controller.destroyMetadata

View File

@ -25,6 +25,7 @@ describe("test the create user action", () => {
expect(res.id).toBeDefined() expect(res.id).toBeDefined()
expect(res.revision).toBeDefined() expect(res.revision).toBeDefined()
const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id) const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id)
expect(userDoc).toBeDefined()
}) })
it("should return an error if no inputs provided", async () => { it("should return an error if no inputs provided", async () => {

View File

@ -33,6 +33,7 @@ exports.USERS_TABLE_SCHEMA = {
type: "table", type: "table",
views: {}, views: {},
name: "Users", name: "Users",
// TODO: ADMIN PANEL - when implemented this doesn't need to be carried out
schema: { schema: {
email: { email: {
type: exports.FieldTypes.STRING, type: exports.FieldTypes.STRING,

View File

@ -127,23 +127,23 @@ exports.generateRowID = (tableId, id = null) => {
/** /**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. * Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/ */
exports.getUserMetadataParams = (email = "", otherProps = {}) => { exports.getUserMetadataParams = (userId = null, otherProps = {}) => {
return exports.getRowParams(InternalTables.USER_METADATA, email, otherProps) return exports.getRowParams(InternalTables.USER_METADATA, userId, otherProps)
} }
/** /**
* Generates a new user ID based on the passed in email. * Generates a new user ID based on the passed in global ID.
* @param {string} email The email which the ID is going to be built up of. * @param {string} globalId The ID of the global user.
* @returns {string} The new user ID which the user doc can be stored under. * @returns {string} The new user ID which the user doc can be stored under.
*/ */
exports.generateUserMetadataID = email => { exports.generateUserMetadataID = globalId => {
return exports.generateRowID(InternalTables.USER_METADATA, email) return exports.generateRowID(InternalTables.USER_METADATA, globalId)
} }
/** /**
* Breaks up the ID to get the email address back out of it. * Breaks up the ID to get the global ID.
*/ */
exports.getEmailFromUserMetadataID = id => { exports.getGlobalIDFromUserMetadataID = id => {
return id.split( return id.split(
`${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
)[1] )[1]

View File

@ -1,8 +1,11 @@
const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth") const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth")
const { getRole } = require("../utilities/security/roles") const { getRole } = require("../utilities/security/roles")
const { generateUserMetadataID } = require("../db/utils")
const { getGlobalUsers } = require("../utilities/workerRequests") const { getGlobalUsers } = require("../utilities/workerRequests")
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles") const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const {
getGlobalIDFromUserMetadataID,
generateUserMetadataID,
} = require("../db/utils")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
// try to get the appID from the request // try to get the appID from the request
@ -27,7 +30,8 @@ module.exports = async (ctx, next) => {
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC) appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
) { ) {
// Different App ID means cookie needs reset, or if the same public user has logged in // 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) const globalId = getGlobalIDFromUserMetadataID(ctx.user.userId)
const globalUser = await getGlobalUsers(ctx, requestAppId, globalId)
updateCookie = true updateCookie = true
appId = requestAppId appId = requestAppId
if (globalUser.roles && globalUser.roles[requestAppId]) { if (globalUser.roles && globalUser.roles[requestAppId]) {
@ -37,22 +41,24 @@ module.exports = async (ctx, next) => {
appId = appCookie.appId appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
} }
if (appId) { // nothing more to do
ctx.appId = appId if (!appId) {
if (roleId) { return next()
const userId = ctx.user }
? generateUserMetadataID(ctx.user.email)
: undefined ctx.appId = appId
ctx.roleId = roleId if (roleId) {
ctx.user = { ctx.roleId = roleId
...ctx.user, const userId = ctx.user ? generateUserMetadataID(ctx.user.userId) : null
_id: userId, ctx.user = {
userId, ...ctx.user,
role: await getRole(appId, roleId), // override userID with metadata one
} _id: userId,
userId,
role: await getRole(appId, roleId),
} }
} }
if (updateCookie && appId) { if (updateCookie) {
setCookie(ctx, { appId, roleId }, Cookies.CurrentApp) setCookie(ctx, { appId, roleId }, Cookies.CurrentApp)
} }
return next() return next()

View File

@ -5,7 +5,7 @@ function mockWorker() {
jest.mock("../../utilities/workerRequests", () => ({ jest.mock("../../utilities/workerRequests", () => ({
getGlobalUsers: () => { getGlobalUsers: () => {
return { return {
email: "test@test.com", email: "us_uuid1",
roles: { roles: {
"app_test": "BASIC", "app_test": "BASIC",
} }
@ -59,7 +59,7 @@ class TestConfiguration {
setUser() { setUser() {
this.ctx.user = { this.ctx.user = {
email: "test@test.com", userId: "ro_ta_user_us_uuid1",
} }
} }

View File

@ -70,8 +70,7 @@ class TestConfiguration {
defaultHeaders() { defaultHeaders() {
const user = { const user = {
userId: "us_test@test.com", userId: "ro_ta_user_us_uuid1",
email: "test@test.com",
builder: { builder: {
global: true, global: true,
}, },
@ -106,12 +105,13 @@ class TestConfiguration {
} }
async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) { async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) {
let user
try { try {
await this.createUser(email, PASSWORD, roleId) user = await this.createUser(email, PASSWORD, roleId)
} catch (err) { } catch (err) {
// allow errors here // allow errors here
} }
return this.login(email, PASSWORD, roleId) return this.login(email, PASSWORD, { roleId, userId: user._id })
} }
async createApp(appName) { async createApp(appName) {
@ -293,33 +293,19 @@ class TestConfiguration {
) )
} }
async makeUserInactive(email) { async login(email, password, { roleId, userId } = {}) {
const user = await this._req( if (!roleId) {
null, roleId = BUILTIN_ROLE_IDS.BUILDER
{ }
email,
},
controllers.user.findMetadata
)
return this._req(
{
...user,
status: "inactive",
},
null,
controllers.user.updateMetadata
)
}
async login(email, password, roleId = BUILTIN_ROLE_IDS.BUILDER) {
if (!this.request) { if (!this.request) {
throw "Server has not been opened, cannot login." throw "Server has not been opened, cannot login."
} }
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
// have to fake this
const user = { const user = {
userId: `us_${email || EMAIL}`, userId: userId || `us_uuid1`,
email: email || EMAIL, email: email || EMAIL,
} }
const app = { const app = {

View File

@ -1,20 +1,18 @@
const CouchDB = require("../db") const CouchDB = require("../db")
const { const { getGlobalIDFromUserMetadataID } = require("../db/utils")
generateUserMetadataID,
getEmailFromUserMetadataID,
} = require("../db/utils")
const { getGlobalUsers } = require("../utilities/workerRequests") const { getGlobalUsers } = require("../utilities/workerRequests")
exports.getFullUser = async ({ ctx, email, userId }) => { exports.getFullUser = async (ctx, userId) => {
if (!email) { const global = await getGlobalUsers(
email = getEmailFromUserMetadataID(userId) ctx,
} ctx.appId,
const global = await getGlobalUsers(ctx, ctx.appId, email) getGlobalIDFromUserMetadataID(userId)
)
let metadata let metadata
try { try {
// this will throw an error if the db doesn't exist, or there is no appId // this will throw an error if the db doesn't exist, or there is no appId
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
metadata = await db.get(generateUserMetadataID(email)) metadata = await db.get(userId)
} catch (err) { } catch (err) {
// it is fine if there is no user metadata, just remove global db info // it is fine if there is no user metadata, just remove global db info
delete global._id delete global._id
@ -24,6 +22,6 @@ exports.getFullUser = async ({ ctx, email, userId }) => {
...global, ...global,
...metadata, ...metadata,
// make sure the ID is always a local ID, not a global one // make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(email), _id: userId,
} }
} }

View File

@ -60,8 +60,8 @@ exports.getDeployedApps = async ctx => {
} }
} }
exports.deleteGlobalUser = async (ctx, email) => { exports.deleteGlobalUser = async (ctx, globalId) => {
const endpoint = `/api/admin/users/${email}` const endpoint = `/api/admin/users/${globalId}`
const reqCfg = { method: "DELETE" } const reqCfg = { method: "DELETE" }
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint), checkSlashesInUrl(env.WORKER_URL + endpoint),
@ -70,8 +70,10 @@ exports.deleteGlobalUser = async (ctx, email) => {
return response.json() return response.json()
} }
exports.getGlobalUsers = async (ctx, appId = null, email = null) => { exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => {
const endpoint = email ? `/api/admin/users/${email}` : `/api/admin/users` const endpoint = globalId
? `/api/admin/users/${globalId}`
: `/api/admin/users`
const reqCfg = { method: "GET" } const reqCfg = { method: "GET" }
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint), checkSlashesInUrl(env.WORKER_URL + endpoint),
@ -89,8 +91,10 @@ exports.getGlobalUsers = async (ctx, appId = null, email = null) => {
return users return users
} }
exports.saveGlobalUser = async (ctx, appId, email, body) => { exports.saveGlobalUser = async (ctx, appId, body) => {
const globalUser = await exports.getGlobalUsers(ctx, appId, email) const globalUser = body._id
? await exports.getGlobalUsers(ctx, appId, body._id)
: {}
const roles = globalUser.roles || {} const roles = globalUser.roles || {}
if (body.roleId) { if (body.roleId) {
roles[appId] = body.roleId roles[appId] = body.roleId
@ -100,9 +104,9 @@ exports.saveGlobalUser = async (ctx, appId, email, body) => {
method: "POST", method: "POST",
body: { body: {
...globalUser, ...globalUser,
email,
password: body.password || undefined, password: body.password || undefined,
status: body.status, status: body.status,
email: body.email,
roles, roles,
builder: { builder: {
global: true, global: true,
@ -124,5 +128,8 @@ exports.saveGlobalUser = async (ctx, appId, email, body) => {
delete body.status delete body.status
delete body.roles delete body.roles
delete body.builder delete body.builder
return body return {
...body,
_id: json._id,
}
} }

View File

@ -31,15 +31,13 @@ exports.fetch = async function(ctx) {
include_docs: true, include_docs: true,
}) })
) )
const groups = response.rows.map(row => row.doc) ctx.body = response.rows.map(row => row.doc)
ctx.body = groups
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
try { try {
const record = await db.get(ctx.params.id) ctx.body = await db.get(ctx.params.id)
ctx.body = record
} catch (err) { } catch (err) {
ctx.throw(err.status, err) ctx.throw(err.status, err)
} }

View File

@ -1,27 +1,42 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { const {
hash, hash,
generateUserID, generateGlobalUserID,
getUserParams, getGlobalUserParams,
StaticDatabases, StaticDatabases,
getGlobalUserByEmail,
} = require("@budibase/auth") } = require("@budibase/auth")
const { UserStatus } = require("../../../constants") const { UserStatus } = require("../../../constants")
const FIRST_USER_EMAIL = "test@test.com"
const FIRST_USER_PASSWORD = "test"
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.userSave = async ctx => { exports.userSave = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const { email, password, _id } = ctx.request.body const { email, password, _id } = ctx.request.body
const hashedPassword = password ? await hash(password) : null
let user = { // make sure another user isn't using the same email
...ctx.request.body, const dbUser = await getGlobalUserByEmail(email)
_id: generateUserID(email), if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
password: hashedPassword, ctx.throw(400, "Email address already in use.")
} }
let dbUser
// in-case user existed already // get the password, make sure one is defined
if (_id) { let hashedPassword
dbUser = await db.get(_id) if (password) {
hashedPassword = await hash(password)
} else if (dbUser) {
hashedPassword = dbUser.password
} else {
ctx.throw(400, "Password must be specified.")
}
let user = {
...dbUser,
...ctx.request.body,
_id: _id || generateGlobalUserID(),
password: hashedPassword,
} }
// add the active status to a user if its not provided // add the active status to a user if its not provided
if (user.status == null) { if (user.status == null) {
@ -29,7 +44,7 @@ exports.userSave = async ctx => {
} }
try { try {
const response = await db.post({ const response = await db.post({
password: hashedPassword || dbUser.password, password: hashedPassword,
...user, ...user,
}) })
ctx.body = { ctx.body = {
@ -46,12 +61,24 @@ exports.userSave = async ctx => {
} }
} }
exports.firstUser = async ctx => {
ctx.request.body = {
email: FIRST_USER_EMAIL,
password: FIRST_USER_PASSWORD,
roles: {},
builder: {
global: true,
},
}
await exports.userSave(ctx)
}
exports.userDelete = async ctx => { exports.userDelete = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const dbUser = await db.get(generateUserID(ctx.params.email)) const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
ctx.body = { ctx.body = {
message: `User ${ctx.params.email} deleted.`, message: `User ${ctx.params.id} deleted.`,
} }
} }
@ -59,7 +86,7 @@ exports.userDelete = async ctx => {
exports.userFetch = async ctx => { exports.userFetch = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs( const response = await db.allDocs(
getUserParams(null, { getGlobalUserParams(null, {
include_docs: true, include_docs: true,
}) })
) )
@ -78,7 +105,7 @@ exports.userFind = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
let user let user
try { try {
user = await db.get(generateUserID(ctx.params.email)) user = await db.get(ctx.params.id)
} catch (err) { } catch (err) {
// no user found, just return nothing // no user found, just return nothing
user = {} user = {}

View File

@ -32,8 +32,9 @@ router
authenticated, authenticated,
controller.userSave controller.userSave
) )
.delete("/api/admin/users/:email", authenticated, controller.userDelete) .post("/api/admin/users/first", controller.firstUser)
.delete("/api/admin/users/:id", authenticated, controller.userDelete)
.get("/api/admin/users", authenticated, controller.userFetch) .get("/api/admin/users", authenticated, controller.userFetch)
.get("/api/admin/users/:email", authenticated, controller.userFind) .get("/api/admin/users/:id", authenticated, controller.userFind)
module.exports = router module.exports = router

View File

@ -1,35 +0,0 @@
exports.StaticDatabases = {
USER: {
name: "user-db",
},
}
const DocumentTypes = {
USER: "us",
APP: "app",
}
exports.DocumentTypes = DocumentTypes
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}`,
}
}