merge with master

This commit is contained in:
Martin McKeaveney 2020-12-07 21:32:45 +00:00
commit efe50230e7
19 changed files with 93 additions and 85 deletions

View File

@ -9,7 +9,7 @@ context('Create a User', () => {
// https://on.cypress.io/interacting-with-elements // https://on.cypress.io/interacting-with-elements
it('should create a user', () => { it('should create a user', () => {
cy.createUser("bbuser", "test", "ADMIN") cy.createUser("bbuser@test.com", "test", "ADMIN")
// // Check to make sure user was created! // // Check to make sure user was created!
cy.contains("bbuser").should('be.visible') cy.contains("bbuser").should('be.visible')

View File

@ -44,9 +44,9 @@ Cypress.Commands.add("createApp", name => {
cy.contains("Next").click() cy.contains("Next").click()
cy.get("input[name=username]") cy.get("input[name=email]")
.click() .click()
.type("test") .type("test@test.com")
cy.get("input[name=password]") cy.get("input[name=password]")
.click() .click()
.type("test") .type("test")
@ -111,7 +111,7 @@ Cypress.Commands.add("addRow", values => {
}) })
}) })
Cypress.Commands.add("createUser", (username, password, accessLevel) => { Cypress.Commands.add("createUser", (email, password, accessLevel) => {
// Create User // Create User
cy.contains("Users").click() cy.contains("Users").click()
@ -123,7 +123,7 @@ Cypress.Commands.add("createUser", (username, password, accessLevel) => {
.type(password) .type(password)
cy.get("input") cy.get("input")
.eq(1) .eq(1)
.type(username) .type(email)
cy.get("select") cy.get("select")
.first() .first()
.select(accessLevel) .select(accessLevel)

View File

@ -62,6 +62,8 @@
</Select> </Select>
{:else if value.customType === 'password'} {:else if value.customType === 'password'}
<Input type="password" extraThin bind:value={block.inputs[key]} /> <Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'email'}
<Input type="email" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'} {:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'} {:else if value.customType === 'row'}

View File

@ -52,7 +52,9 @@
applicationName: string().required("Your application must have a name."), applicationName: string().required("Your application must have a name."),
}, },
{ {
username: string().required("Your application needs a first user."), email: string()
.email()
.required("Your application needs a first user."),
password: string().required( password: string().required(
"Please enter a password for your first user." "Please enter a password for your first user."
), ),
@ -164,8 +166,7 @@
// Create user // Create user
const user = { const user = {
name: $createAppStore.values.username, email: $createAppStore.values.email,
username: $createAppStore.values.username,
password: $createAppStore.values.password, password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId, accessLevelId: $createAppStore.values.accessLevelId,
} }

View File

@ -2,18 +2,18 @@
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let validationErrors export let validationErrors
let blurred = { username: false, password: false } let blurred = { email: false, password: false }
</script> </script>
<h2>Create your first User</h2> <h2>Create your first User</h2>
<div class="container"> <div class="container">
<Input <Input
on:input={() => (blurred.username = true)} on:input={() => (blurred.email = true)}
label="Username" label="Email"
name="username" name="email"
placeholder="Username" placeholder="Email"
type="name" type="email"
error={blurred.username && validationErrors.username} /> error={blurred.email && validationErrors.email} />
<Input <Input
on:input={() => (blurred.password = true)} on:input={() => (blurred.password = true)}
label="Password" label="Password"

View File

@ -3,7 +3,7 @@ export const TableNames = {
} }
// fields on the user table that cannot be edited // fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["username", "password", "accessLevelId"] export const UNEDITABLE_USER_FIELDS = ["email", "password", "accessLevelId"]
export const DEFAULT_PAGES_OBJECT = { export const DEFAULT_PAGES_OBJECT = {
main: { main: {

View File

@ -3,15 +3,15 @@ import API from "./api"
/** /**
* Performs a log in request. * Performs a log in request.
*/ */
export const logIn = async ({ username, password }) => { export const logIn = async ({ email, password }) => {
if (!username) { if (!email) {
return API.error("Please enter your username") return API.error("Please enter your email")
} }
if (!password) { if (!password) {
return API.error("Please enter your password") return API.error("Please enter your password")
} }
return await API.post({ return await API.post({
url: "/api/authenticate", url: "/api/authenticate",
body: { username, password }, body: { email, password },
}) })
} }

View File

@ -5,8 +5,8 @@ import { writable } from "svelte/store"
const createAuthStore = () => { const createAuthStore = () => {
const store = writable("") const store = writable("")
const logIn = async ({ username, password }) => { const logIn = async ({ email, password }) => {
const user = await API.logIn({ username, password }) const user = await API.logIn({ email, password })
if (!user.error) { if (!user.error) {
store.set(user.token) store.set(user.token)
location.reload() location.reload()

View File

@ -10,21 +10,21 @@ exports.authenticate = async ctx => {
const appId = ctx.appId const appId = ctx.appId
if (!appId) ctx.throw(400, "No appId") if (!appId) ctx.throw(400, "No appId")
const { username, password } = ctx.request.body const { email, password } = ctx.request.body
if (!username) ctx.throw(400, "Username Required.") if (!email) ctx.throw(400, "Email Required.")
if (!password) ctx.throw(400, "Password Required.") if (!password) ctx.throw(400, "Password Required.")
// Check the user exists in the instance DB by username // Check the user exists in the instance DB by email
const db = new CouchDB(appId) const db = new CouchDB(appId)
const app = await db.get(appId) const app = await db.get(appId)
let dbUser let dbUser
try { try {
dbUser = await db.get(generateUserID(username)) dbUser = await db.get(generateUserID(email))
} catch (_) { } catch (_) {
// do not want to throw a 404 - as this could be // do not want to throw a 404 - as this could be
// used to determine valid usernames // used to determine valid emails
ctx.throw(401, "Invalid Credentials") ctx.throw(401, "Invalid Credentials")
} }

View File

@ -20,16 +20,10 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const { const { email, password, accessLevelId, permissions } = ctx.request.body
username,
password,
name,
accessLevelId,
permissions,
} = ctx.request.body
if (!username || !password) { if (!email || !password) {
ctx.throw(400, "Username and Password Required.") ctx.throw(400, "email and Password Required.")
} }
const accessLevel = await checkAccessLevel(db, accessLevelId) const accessLevel = await checkAccessLevel(db, accessLevelId)
@ -37,10 +31,9 @@ exports.create = async function(ctx) {
if (!accessLevel) ctx.throw(400, "Invalid Access Level") if (!accessLevel) ctx.throw(400, "Invalid Access Level")
const user = { const user = {
_id: generateUserID(username), _id: generateUserID(email),
username, email,
password: await bcrypt.hash(password), password: await bcrypt.hash(password),
name: name || username,
type: "user", type: "user",
accessLevelId, accessLevelId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER], permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
@ -54,8 +47,7 @@ exports.create = async function(ctx) {
ctx.userId = response._id ctx.userId = response._id
ctx.body = { ctx.body = {
_rev: response.rev, _rev: response.rev,
username, email,
name,
} }
} catch (err) { } catch (err) {
if (err.status === 409) { if (err.status === 409) {
@ -76,22 +68,22 @@ exports.update = async function(ctx) {
user._rev = response.rev user._rev = response.rev
ctx.status = 200 ctx.status = 200
ctx.message = `User ${ctx.request.body.username} updated successfully.` ctx.message = `User ${ctx.request.body.email} updated successfully.`
ctx.body = response ctx.body = response
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.appId) const database = new CouchDB(ctx.user.appId)
await database.destroy(generateUserID(ctx.params.username)) await database.destroy(generateUserID(ctx.params.email))
ctx.message = `User ${ctx.params.username} deleted.` ctx.message = `User ${ctx.params.email} deleted.`
ctx.status = 200 ctx.status = 200
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId) const database = new CouchDB(ctx.user.appId)
const user = await database.get(generateUserID(ctx.params.username)) const user = await database.get(generateUserID(ctx.params.email))
ctx.body = { ctx.body = {
username: user.username, email: user.email,
name: user.name, name: user.name,
_rev: user._rev, _rev: user._rev,
} }

View File

@ -118,7 +118,7 @@ exports.clearApplications = async request => {
exports.createUser = async ( exports.createUser = async (
request, request,
appId, appId,
username = "babs", email = "babs@babs.com",
password = "babs_password" password = "babs_password"
) => { ) => {
const res = await request const res = await request
@ -126,7 +126,7 @@ exports.createUser = async (
.set(exports.defaultHeaders(appId)) .set(exports.defaultHeaders(appId))
.send({ .send({
name: "Bill", name: "Bill",
username, email,
password, password,
accessLevelId: BUILTIN_LEVEL_IDS.POWER, accessLevelId: BUILTIN_LEVEL_IDS.POWER,
}) })
@ -174,15 +174,14 @@ const createUserWithPermissions = async (
request, request,
appId, appId,
permissions, permissions,
username email
) => { ) => {
const password = `password_${username}` const password = `password_${email}`
await request await request
.post(`/api/users`) .post(`/api/users`)
.set(exports.defaultHeaders(appId)) .set(exports.defaultHeaders(appId))
.send({ .send({
name: username, email,
username,
password, password,
accessLevelId: BUILTIN_LEVEL_IDS.POWER, accessLevelId: BUILTIN_LEVEL_IDS.POWER,
permissions, permissions,
@ -203,7 +202,7 @@ const createUserWithPermissions = async (
Cookie: `budibase:${appId}:local=${anonToken}`, Cookie: `budibase:${appId}:local=${anonToken}`,
"x-budibase-app-id": appId, "x-budibase-app-id": appId,
}) })
.send({ username, password }) .send({ email, password })
// returning necessary request headers // returning necessary request headers
return { return {

View File

@ -34,8 +34,8 @@ describe("/users", () => {
describe("fetch", () => { describe("fetch", () => {
it("returns a list of users from an instance db", async () => { it("returns a list of users from an instance db", async () => {
await createUser(request, appId, "brenda", "brendas_password") await createUser(request, appId, "brenda@brenda.com", "brendas_password")
await createUser(request, appId, "pam", "pam_password") await createUser(request, appId, "pam@pam.com", "pam_password")
const res = await request const res = await request
.get(`/api/users`) .get(`/api/users`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
@ -43,12 +43,12 @@ describe("/users", () => {
.expect(200) .expect(200)
expect(res.body.length).toBe(2) expect(res.body.length).toBe(2)
expect(res.body.find(u => u.username === "brenda")).toBeDefined() expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined()
expect(res.body.find(u => u.username === "pam")).toBeDefined() expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined()
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
await createUser(request, appId, "brenda", "brendas_password") await createUser(request, appId, "brenda@brenda.com", "brendas_password")
await testPermissionsForEndpoint({ await testPermissionsForEndpoint({
request, request,
method: "GET", method: "GET",
@ -62,12 +62,11 @@ describe("/users", () => {
}) })
describe("create", () => { describe("create", () => {
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 res = await request const res = await request
.post(`/api/users`) .post(`/api/users`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER }) .send({ email: "bill@bill.com", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER })
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -79,7 +78,7 @@ describe("/users", () => {
await testPermissionsForEndpoint({ await testPermissionsForEndpoint({
request, request,
method: "POST", method: "POST",
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER }, body: { email: "brandNewUser@user.com", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER },
url: `/api/users`, url: `/api/users`,
appId: appId, appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.ADMIN, permName1: BUILTIN_PERMISSION_NAMES.ADMIN,

View File

@ -16,7 +16,7 @@ router
controller.fetch controller.fetch
) )
.get( .get(
"/api/users/:username", "/api/users/:email",
authorized(PermissionTypes.USER, PermissionLevels.READ), authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.find controller.find
) )
@ -32,7 +32,7 @@ router
controller.create controller.create
) )
.delete( .delete(
"/api/users/:username", "/api/users/:email",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage, usage,
controller.destroy controller.destroy

View File

@ -1,4 +1,5 @@
const Koa = require("koa") const Koa = require("koa")
const destroyable = require("server-destroy")
const electron = require("electron") const electron = require("electron")
const koaBody = require("koa-body") const koaBody = require("koa-body")
const logger = require("koa-pino-logger") const logger = require("koa-pino-logger")
@ -44,6 +45,7 @@ if (electron.app && electron.app.isPackaged) {
} }
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
destroyable(server)
server.on("close", () => console.log("Server Closed")) server.on("close", () => console.log("Server Closed"))
@ -51,3 +53,14 @@ module.exports = server.listen(env.PORT || 4001, () => {
console.log(`Budibase running on ${JSON.stringify(server.address())}`) console.log(`Budibase running on ${JSON.stringify(server.address())}`)
automations.init() automations.init()
}) })
process.on("uncaughtException", err => {
console.error(err)
server.close()
server.destroy()
})
process.on("SIGTERM", () => {
server.close()
server.destroy()
})

View File

@ -5,7 +5,7 @@ const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
description: "Create a new user", description: "Create a new user",
tagline: "Create user {{inputs.username}}", tagline: "Create user {{inputs.email}}",
icon: "ri-user-add-line", icon: "ri-user-add-line",
name: "Create User", name: "Create User",
type: "ACTION", type: "ACTION",
@ -16,9 +16,10 @@ module.exports.definition = {
schema: { schema: {
inputs: { inputs: {
properties: { properties: {
username: { email: {
type: "string", type: "string",
title: "Username", customType: "email",
title: "Email",
}, },
password: { password: {
type: "string", type: "string",
@ -32,7 +33,7 @@ module.exports.definition = {
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY, pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
}, },
}, },
required: ["username", "password", "accessLevelId"], required: ["email", "password", "accessLevelId"],
}, },
outputs: { outputs: {
properties: { properties: {
@ -59,13 +60,13 @@ module.exports.definition = {
} }
module.exports.run = async function({ inputs, appId, apiKey, emitter }) { module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
const { username, password, accessLevelId } = inputs const { email, password, accessLevelId } = inputs
const ctx = { const ctx = {
user: { user: {
appId: appId, appId: appId,
}, },
request: { request: {
body: { username, password, accessLevelId }, body: { email, password, accessLevelId },
}, },
eventEmitter: emitter, eventEmitter: emitter,
} }

View File

@ -12,17 +12,18 @@ const USERS_TABLE_SCHEMA = {
views: {}, views: {},
name: "Users", name: "Users",
schema: { schema: {
username: { email: {
type: "string", type: "string",
constraints: { constraints: {
type: "string", type: "string",
email: true,
length: { length: {
maximum: "", maximum: "",
}, },
presence: true, presence: true,
}, },
fieldName: "username", fieldName: "email",
name: "username", name: "email",
}, },
accessLevelId: { accessLevelId: {
fieldName: "accessLevelId", fieldName: "accessLevelId",
@ -35,7 +36,7 @@ const USERS_TABLE_SCHEMA = {
}, },
}, },
}, },
primaryDisplay: "username", primaryDisplay: "email",
} }
exports.AuthTypes = AuthTypes exports.AuthTypes = AuthTypes

View File

@ -101,21 +101,21 @@ exports.generateRowID = tableId => {
/** /**
* 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.getUserParams = (username = "", otherProps = {}) => { exports.getUserParams = (email = "", otherProps = {}) => {
return getDocParams( return getDocParams(
DocumentTypes.ROW, DocumentTypes.ROW,
`${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}`, `${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}`,
otherProps otherProps
) )
} }
/** /**
* Generates a new user ID based on the passed in username. * Generates a new user ID based on the passed in username.
* @param {string} username The username which the ID is going to be built up of. * @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. * @returns {string} The new user ID which the user doc can be stored under.
*/ */
exports.generateUserID = username => { exports.generateUserID = email => {
return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}` return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}`
} }
/** /**

View File

@ -56,7 +56,7 @@
}, },
"login": { "login": {
"name": "Login Control", "name": "Login Control",
"description": "A control that accepts username, password an also handles password resets", "description": "A control that accepts email, password an also handles password resets",
"props": { "props": {
"logo": "string", "logo": "string",
"title": "string", "title": "string",

View File

@ -10,7 +10,7 @@
export let buttonClass = "" export let buttonClass = ""
export let inputClass = "" export let inputClass = ""
let username = "" let email = ""
let password = "" let password = ""
let loading = false let loading = false
let error = false let error = false
@ -24,7 +24,7 @@
const login = async () => { const login = async () => {
loading = true loading = true
await authStore.actions.logIn({ username, password }) await authStore.actions.logIn({ email, password })
loading = false loading = false
} }
</script> </script>
@ -42,9 +42,9 @@
<div class="form-root"> <div class="form-root">
<div class="control"> <div class="control">
<input <input
bind:value={username} bind:value={email}
type="text" type="email"
placeholder="Username" placeholder="Email"
class={_inputClass} /> class={_inputClass} />
</div> </div>
@ -62,7 +62,7 @@
</div> </div>
{#if error} {#if error}
<div class="incorrect-details-panel">Incorrect username or password</div> <div class="incorrect-details-panel">Incorrect email or password</div>
{/if} {/if}
</div> </div>
</div> </div>