diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab2e7d597b..9d473198b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -166,6 +166,18 @@ rm -rf ~/.budibase ``` Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation. +### Running tests + +#### End-to-end Tests + +Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder: + +``` +yarn test:e2e +``` + +Or if you are in the builder you can run `yarn cy:test`. + ### Other Useful Information diff --git a/package.json b/package.json index 443c8cccc3..8185d0bbb0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@rollup/plugin-json": "^4.0.2", "babel-eslint": "^10.0.3", "eslint": "^6.8.0", + "eslint-plugin-cypress": "^2.11.1", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-svelte3": "^2.7.3", "lerna": "3.14.1", @@ -24,10 +25,11 @@ "test": "lerna run test", "lint": "eslint packages", "lint:fix": "eslint --fix packages", - "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"" + "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"", + "test:e2e": "lerna run cy:test" }, "dependencies": { "@material/icon-button": "4.0.0", "date-fns": "^2.10.0" } -} \ No newline at end of file +} diff --git a/packages/builder/.gitignore b/packages/builder/.gitignore index 94ba827047..49444a4fc0 100644 --- a/packages/builder/.gitignore +++ b/packages/builder/.gitignore @@ -5,4 +5,5 @@ package-lock.json yarn.lock release/ dist/ +cypress/screenshots routify diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json new file mode 100644 index 0000000000..477d0a6dc4 --- /dev/null +++ b/packages/builder/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:4001/_builder/", + "video": false +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/example.json b/packages/builder/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/packages/builder/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/profile.json b/packages/builder/cypress/fixtures/profile.json new file mode 100644 index 0000000000..b6c355ca5c --- /dev/null +++ b/packages/builder/cypress/fixtures/profile.json @@ -0,0 +1,5 @@ +{ + "id": 8739, + "name": "Jane", + "email": "jane@example.com" +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/users.json b/packages/builder/cypress/fixtures/users.json new file mode 100644 index 0000000000..79b699aa77 --- /dev/null +++ b/packages/builder/cypress/fixtures/users.json @@ -0,0 +1,232 @@ +[ + { + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "address": { + "street": "Kulas Light", + "suite": "Apt. 556", + "city": "Gwenborough", + "zipcode": "92998-3874", + "geo": { + "lat": "-37.3159", + "lng": "81.1496" + } + }, + "phone": "1-770-736-8031 x56442", + "website": "hildegard.org", + "company": { + "name": "Romaguera-Crona", + "catchPhrase": "Multi-layered client-server neural-net", + "bs": "harness real-time e-markets" + } + }, + { + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", + "address": { + "street": "Victor Plains", + "suite": "Suite 879", + "city": "Wisokyburgh", + "zipcode": "90566-7771", + "geo": { + "lat": "-43.9509", + "lng": "-34.4618" + } + }, + "phone": "010-692-6593 x09125", + "website": "anastasia.net", + "company": { + "name": "Deckow-Crist", + "catchPhrase": "Proactive didactic contingency", + "bs": "synergize scalable supply-chains" + } + }, + { + "id": 3, + "name": "Clementine Bauch", + "username": "Samantha", + "email": "Nathan@yesenia.net", + "address": { + "street": "Douglas Extension", + "suite": "Suite 847", + "city": "McKenziehaven", + "zipcode": "59590-4157", + "geo": { + "lat": "-68.6102", + "lng": "-47.0653" + } + }, + "phone": "1-463-123-4447", + "website": "ramiro.info", + "company": { + "name": "Romaguera-Jacobson", + "catchPhrase": "Face to face bifurcated interface", + "bs": "e-enable strategic applications" + } + }, + { + "id": 4, + "name": "Patricia Lebsack", + "username": "Karianne", + "email": "Julianne.OConner@kory.org", + "address": { + "street": "Hoeger Mall", + "suite": "Apt. 692", + "city": "South Elvis", + "zipcode": "53919-4257", + "geo": { + "lat": "29.4572", + "lng": "-164.2990" + } + }, + "phone": "493-170-9623 x156", + "website": "kale.biz", + "company": { + "name": "Robel-Corkery", + "catchPhrase": "Multi-tiered zero tolerance productivity", + "bs": "transition cutting-edge web services" + } + }, + { + "id": 5, + "name": "Chelsey Dietrich", + "username": "Kamren", + "email": "Lucio_Hettinger@annie.ca", + "address": { + "street": "Skiles Walks", + "suite": "Suite 351", + "city": "Roscoeview", + "zipcode": "33263", + "geo": { + "lat": "-31.8129", + "lng": "62.5342" + } + }, + "phone": "(254)954-1289", + "website": "demarco.info", + "company": { + "name": "Keebler LLC", + "catchPhrase": "User-centric fault-tolerant solution", + "bs": "revolutionize end-to-end systems" + } + }, + { + "id": 6, + "name": "Mrs. Dennis Schulist", + "username": "Leopoldo_Corkery", + "email": "Karley_Dach@jasper.info", + "address": { + "street": "Norberto Crossing", + "suite": "Apt. 950", + "city": "South Christy", + "zipcode": "23505-1337", + "geo": { + "lat": "-71.4197", + "lng": "71.7478" + } + }, + "phone": "1-477-935-8478 x6430", + "website": "ola.org", + "company": { + "name": "Considine-Lockman", + "catchPhrase": "Synchronised bottom-line interface", + "bs": "e-enable innovative applications" + } + }, + { + "id": 7, + "name": "Kurtis Weissnat", + "username": "Elwyn.Skiles", + "email": "Telly.Hoeger@billy.biz", + "address": { + "street": "Rex Trail", + "suite": "Suite 280", + "city": "Howemouth", + "zipcode": "58804-1099", + "geo": { + "lat": "24.8918", + "lng": "21.8984" + } + }, + "phone": "210.067.6132", + "website": "elvis.io", + "company": { + "name": "Johns Group", + "catchPhrase": "Configurable multimedia task-force", + "bs": "generate enterprise e-tailers" + } + }, + { + "id": 8, + "name": "Nicholas Runolfsdottir V", + "username": "Maxime_Nienow", + "email": "Sherwood@rosamond.me", + "address": { + "street": "Ellsworth Summit", + "suite": "Suite 729", + "city": "Aliyaview", + "zipcode": "45169", + "geo": { + "lat": "-14.3990", + "lng": "-120.7677" + } + }, + "phone": "586.493.6943 x140", + "website": "jacynthe.com", + "company": { + "name": "Abernathy Group", + "catchPhrase": "Implemented secondary concept", + "bs": "e-enable extensible e-tailers" + } + }, + { + "id": 9, + "name": "Glenna Reichert", + "username": "Delphine", + "email": "Chaim_McDermott@dana.io", + "address": { + "street": "Dayna Park", + "suite": "Suite 449", + "city": "Bartholomebury", + "zipcode": "76495-3109", + "geo": { + "lat": "24.6463", + "lng": "-168.8889" + } + }, + "phone": "(775)976-6794 x41206", + "website": "conrad.com", + "company": { + "name": "Yost and Sons", + "catchPhrase": "Switchable contextually-based project", + "bs": "aggregate real-time technologies" + } + }, + { + "id": 10, + "name": "Clementina DuBuque", + "username": "Moriah.Stanton", + "email": "Rey.Padberg@karina.biz", + "address": { + "street": "Kattie Turnpike", + "suite": "Suite 198", + "city": "Lebsackbury", + "zipcode": "31428-2261", + "geo": { + "lat": "-38.2386", + "lng": "57.2232" + } + }, + "phone": "024-648-3804", + "website": "ambrose.net", + "company": { + "name": "Hoeger LLC", + "catchPhrase": "Centralized empowering task-force", + "bs": "target end-to-end models" + } + } +] \ No newline at end of file diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js new file mode 100644 index 0000000000..07a8687ea8 --- /dev/null +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -0,0 +1,17 @@ +context('Create an Application', () => { + + beforeEach(() => { + cy.visit('localhost:4001/_builder') + }) + + // https://on.cypress.io/interacting-with-elements + + it('should create a new application', () => { + // https://on.cypress.io/type + cy.createApp('My Cool App', 'This is a description') + + cy.visit('localhost:4001/_builder') + + cy.contains('My Cool App').should('exist') + }) +}) diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js new file mode 100644 index 0000000000..cedd8fefe1 --- /dev/null +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -0,0 +1,53 @@ +context('Create Components', () => { + + before(() => { + cy.visit('localhost:4001/_builder') + // https://on.cypress.io/type + cy.createApp('Model App', 'Model App Description') + cy.createModel('dog', 'name', 'age') + cy.addRecord('bob', '15') + }) + + // https://on.cypress.io/interacting-with-elements + it('should add a container', () => { + cy.contains('frontend').click() + cy.get('.switcher > :nth-child(2)').click() + + cy.contains('Container').click() + }) + it('should add a headline', () => { + cy.addHeadlineComponent('An Amazing headline!') + + getIframeBody().contains('An Amazing headline!') + }) + it('change the font size of the headline', () => { + cy.contains('Typography').click() + cy.get('input[name="font-size"]') + .type('60px') + cy.contains('Design').click() + + getIframeBody().contains('An Amazing headline!').should('have.css', 'font-size', '60px') + }) +}) + +const getIframeDocument = () => { + return cy + .get('iframe') + // Cypress yields jQuery element, which has the real + // DOM element under property "0". + // From the real DOM iframe element we can get + // the "document" element, it is stored in "contentDocument" property + // Cypress "its" command can access deep properties using dot notation + // https://on.cypress.io/its + .its('0.contentDocument').should('exist') +} + +const getIframeBody = () => { + // get the document + return getIframeDocument() + // automatically retries until body is loaded + .its('body').should('not.be.undefined') + // wraps "body" DOM element to allow + // chaining more Cypress commands, like ".find(...)" + .then(cy.wrap) +} \ No newline at end of file diff --git a/packages/builder/cypress/integration/createModel.spec.js b/packages/builder/cypress/integration/createModel.spec.js new file mode 100644 index 0000000000..830f5e7a48 --- /dev/null +++ b/packages/builder/cypress/integration/createModel.spec.js @@ -0,0 +1,22 @@ +context('Create a Model', () => { + + before(() => { + cy.visit('localhost:4001/_builder') + // https://on.cypress.io/type + cy.createApp('Model App', 'Model App Description') + }) + + // https://on.cypress.io/interacting-with-elements + it('should create a new model', () => { + + cy.createModel('dog', 'name', 'age') + + // Check if model exists + cy.get('.title').should('have.text', 'dog') + }) + it('should add a record', () => { + cy.addRecord('bob', '15') + + cy.contains('bob').should('have.text', 'bob') + }) +}) diff --git a/packages/builder/cypress/integration/createUser.spec.js b/packages/builder/cypress/integration/createUser.spec.js new file mode 100644 index 0000000000..80d1a51d84 --- /dev/null +++ b/packages/builder/cypress/integration/createUser.spec.js @@ -0,0 +1,19 @@ +context('Create a User', () => { + + before(() => { + cy.visit('localhost:4001/_builder') + // https://on.cypress.io/type + cy.createApp('User App', 'This app is used to test user creation') + }) + + // https://on.cypress.io/interacting-with-elements + it('should create a user', () => { + // Close Model modal that shows up after creating an app + cy.get('.close').click() + + cy.createUser('bbuser', 'test', 'ADMIN') + + // Check to make sure user was created! + cy.contains('bbuser').should('have.text', 'bbuser') + }) +}) diff --git a/packages/builder/cypress/integration/createWorkflow.spec.js b/packages/builder/cypress/integration/createWorkflow.spec.js new file mode 100644 index 0000000000..9188721988 --- /dev/null +++ b/packages/builder/cypress/integration/createWorkflow.spec.js @@ -0,0 +1,46 @@ +context('Create a workflow', () => { + + before(() => { + cy.visit('localhost:4001/_builder') + + cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!') + }) + + // https://on.cypress.io/interacting-with-elements + it('should create a workflow', () => { + cy.createModel('dog', 'name', 'age') + cy.createUser('bbuser', 'test', 'ADMIN') + + + cy.contains('workflow').click() + cy.get('.new-workflow-button').click() + cy.get('input').type('Add Record') + cy.contains('Save').click() + + // Add trigger + cy.get('[data-cy=add-workflow-component]').click() + cy.get('[data-cy=RECORD_SAVED]').click() + cy.get('.budibase__input').select('dog') + + // Create action + cy.get('[data-cy=SAVE_RECORD]').click() + cy.get(':nth-child(2) > .budibase__input').type('goodboy') + cy.get(':nth-child(3) > .budibase__input').type('11') + + // Save + cy.get('[data-cy=save-workflow-setup]').click() + cy.get('.workflow-button').click() + + // Activate Workflow + cy.get('[data-cy=activate-workflow]').click() + + }) + it('should add record when a new record is added', () => { + cy.contains('backend').click() + + cy.addRecord('bob', '15') + + cy.contains('goodboy').should('have.text', 'goodboy') + + }) +}) \ No newline at end of file diff --git a/packages/builder/cypress/plugins/index.js b/packages/builder/cypress/plugins/index.js new file mode 100644 index 0000000000..59b2bab6e4 --- /dev/null +++ b/packages/builder/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js new file mode 100644 index 0000000000..80516471da --- /dev/null +++ b/packages/builder/cypress/setup.js @@ -0,0 +1,17 @@ +// What this script does: +// 1. Removes the old test folder if it exists (.budibase-cypress) +// 2. Initialises using `.budibase-cypress` +// 3. Runs the server using said folder + +const rimraf = require("rimraf") +const { join } = require("path") +const homedir = join(require("os").homedir(), ".budibase-cypress") +const init = require("../../cli/src/commands/init/initHandler") +const run = require("../../cli/src/commands/run/runHandler") + +rimraf.sync(homedir) + +init({ dir: homedir, clientId: "cypress-test" }).then(() => { + delete require.cache[require.resolve("../../server/src/environment")] + run({ dir: homedir }) +}) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js new file mode 100644 index 0000000000..ed19ba8233 --- /dev/null +++ b/packages/builder/cypress/support/commands.js @@ -0,0 +1,105 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// 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("createApp", (name, description) => { + cy.get(".banner-button") + .click() + .get('input[name="name"]') + .type(name) + .should("have.value", name) + + cy.get('textarea[name="description"]') + .type(description) + .should("have.value", description) + + cy.contains("Save").click() +}) +Cypress.Commands.add("createModel", (modelName, firstField, secondField) => { + // Enter model name + cy.get("[data-cy=Name]") + .click() + .type(modelName) + + // Add 'name' field + cy.get("[data-cy=add-new-model-field]").click() + cy.get("[data-cy=Name]") + .click() + .type(firstField) + cy.contains("Save").click() + + // Add 'age' field + cy.get("[data-cy=add-new-model-field]").click() + + cy.get("[data-cy=Name]") + .click() + .type(secondField) + cy.get("select").select("number") + cy.contains("Save").click() + cy.contains(secondField).should("exist") + + // Save model + cy.contains("Save").click() +}) +Cypress.Commands.add("addRecord", (firstField, secondField) => { + cy.contains("Create new record").click() + + cy.get("[data-cy=name-input]") + .click() + .type(firstField) + cy.get("[data-cy=age-input]") + .click() + .type(secondField) + + // Save + cy.contains("Save").click() +}) + +Cypress.Commands.add("createUser", (username, password, level) => { + // Create User + cy.get(".nav-group-header > .ri-add-line").click() + + cy.get("[data-cy=username]").type(username) + cy.get("[data-cy=password]").type(password) + cy.get("[data-cy=accessLevel]").select(level) + + // Save + cy.contains("Save").click() +}) + +Cypress.Commands.add("addHeadlineComponent", text => { + cy.get(".switcher > :nth-child(2)").click() + + cy.get("[data-cy=Text]").click() + cy.get("[data-cy=Headline]").click() + cy.get(".tabs > :nth-child(2)").click() + cy.get('input[type="text"]').type(text) + cy.contains("Design").click() +}) +Cypress.Commands.add("addButtonComponent", () => { + cy.get(".switcher > :nth-child(2)").click() + + cy.get("[data-cy=Button]").click() +}) diff --git a/packages/builder/cypress/support/cookies.js b/packages/builder/cypress/support/cookies.js new file mode 100644 index 0000000000..bfb470c57b --- /dev/null +++ b/packages/builder/cypress/support/cookies.js @@ -0,0 +1,3 @@ +Cypress.Cookies.defaults({ + whitelist: "builder:token", +}) diff --git a/packages/builder/cypress/support/index.js b/packages/builder/cypress/support/index.js new file mode 100644 index 0000000000..15c9d759fc --- /dev/null +++ b/packages/builder/cypress/support/index.js @@ -0,0 +1,21 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./cookies" +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/packages/builder/package.json b/packages/builder/package.json index e681253de8..80038d3ceb 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -9,7 +9,11 @@ "test": "jest", "test:watch": "jest --watchAll", "dev:builder": "routify --routify-dir routify -c rollup", - "rollup": "rollup -c -w" + "rollup": "rollup -c -w", + "cy:setup": "node ./cypress/setup.js", + "cy:run": "cypress run", + "cy:open": "cypress open", + "cy:test": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run" }, "jest": { "globals": { @@ -35,6 +39,14 @@ }, "transformIgnorePatterns": [ "/node_modules/(?!svelte).+\\.js$" + ], + "modulePathIgnorePatterns": [ + "/cypress/" + ] + }, + "eslintConfig": { + "extends": [ + "plugin:cypress/recommended" ] }, "dependencies": { @@ -67,10 +79,12 @@ "@sveltech/routify": "1.7.11", "babel-jest": "^24.8.0", "browser-sync": "^2.26.7", + "cypress": "^4.8.0", "http-proxy-middleware": "^0.19.1", "jest": "^24.8.0", "ncp": "^2.0.0", "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", "rollup": "^1.12.0", "rollup-plugin-alias": "^1.5.2", "rollup-plugin-browsersync": "^1.0.0", @@ -83,6 +97,7 @@ "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^4.0.4", "rollup-plugin-url": "^2.2.2", + "start-server-and-test": "^1.11.0", "svelte": "3.23.x" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js index 3fcd35ce28..116222409f 100644 --- a/packages/builder/src/builderStore/api.js +++ b/packages/builder/src/builderStore/api.js @@ -1,17 +1,13 @@ const apiCall = method => async (url, body) => { + const headers = { + "Content-Type": "application/json", + } const response = await fetch(url, { method: method, - headers: { - "Content-Type": "application/json", - "x-user-agent": "Budibase Builder", - }, body: body && JSON.stringify(body), + headers, }) - // if (response.status === 500) { - // throw new Error("Server Error"); - // } - return response } @@ -22,9 +18,9 @@ export const del = apiCall("DELETE") export const put = apiCall("PUT") export default { - post, - get, - patch, - delete: del, - put, + post: apiCall("POST"), + get: apiCall("GET"), + patch: apiCall("PATCH"), + delete: apiCall("DELETE"), + put: apiCall("PUT"), } diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 9a5a7b1fbc..53bdaa3360 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -24,8 +24,8 @@ export const getBackendUiStore = () => { store.actions = { database: { select: async db => { - const modelsResponse = await api.get(`/api/${db._id}/models`) - const viewsResponse = await api.get(`/api/${db._id}/views`) + const modelsResponse = await api.get(`/api/models`) + const viewsResponse = await api.get(`/api/views`) const models = await modelsResponse.json() const views = await viewsResponse.json() store.update(state => { diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js index 1f3ea9c0ef..0b17e8c61c 100644 --- a/packages/builder/src/builderStore/store/index.js +++ b/packages/builder/src/builderStore/store/index.js @@ -1,5 +1,4 @@ import { values } from "lodash/fp" -import { backendUiStore } from "builderStore" import * as backendStoreActions from "./backend" import { writable, get } from "svelte/store" import api from "../api" @@ -295,13 +294,10 @@ const addChildComponent = store => (componentToAdd, presetName) => { const presetProps = presetName ? component.presets[presetName] : {} - const instanceId = get(backendUiStore).selectedDatabase._id - const newComponent = createProps( component, { ...presetProps, - _instanceId: instanceId, }, state ) diff --git a/packages/builder/src/builderStore/store/workflow/index.js b/packages/builder/src/builderStore/store/workflow/index.js index 8d5e63b197..9d50f85661 100644 --- a/packages/builder/src/builderStore/store/workflow/index.js +++ b/packages/builder/src/builderStore/store/workflow/index.js @@ -3,8 +3,8 @@ import api from "../../api" import Workflow from "./Workflow" const workflowActions = store => ({ - fetch: async instanceId => { - const WORKFLOWS_URL = `/api/${instanceId}/workflows` + fetch: async () => { + const WORKFLOWS_URL = `/api/workflows` const workflowResponse = await api.get(WORKFLOWS_URL) const json = await workflowResponse.json() store.update(state => { @@ -12,14 +12,14 @@ const workflowActions = store => ({ return state }) }, - create: async ({ instanceId, name }) => { + create: async ({ name }) => { const workflow = { name, definition: { steps: [], }, } - const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + const CREATE_WORKFLOW_URL = `/api/workflows` const response = await api.post(CREATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -28,8 +28,8 @@ const workflowActions = store => ({ return state }) }, - save: async ({ instanceId, workflow }) => { - const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + save: async ({ workflow }) => { + const UPDATE_WORKFLOW_URL = `/api/workflows` const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -42,8 +42,8 @@ const workflowActions = store => ({ return state }) }, - update: async ({ instanceId, workflow }) => { - const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + update: async ({ workflow }) => { + const UPDATE_WORKFLOW_URL = `/api/workflows` const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -55,9 +55,9 @@ const workflowActions = store => ({ return state }) }, - delete: async ({ instanceId, workflow }) => { + delete: async ({ workflow }) => { const { _id, _rev } = workflow - const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}` + const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}` await api.delete(DELETE_WORKFLOW_URL) store.update(state => { diff --git a/packages/builder/src/components/common/Input.svelte b/packages/builder/src/components/common/Input.svelte index 2797092d07..730a9ed043 100644 --- a/packages/builder/src/components/common/Input.svelte +++ b/packages/builder/src/components/common/Input.svelte @@ -2,6 +2,7 @@ import { onMount } from "svelte" import { buildStyle } from "../../helpers.js" export let value = "" + export let name = "" export let textAlign = "left" export let width = "160px" export let placeholder = "" @@ -25,6 +26,7 @@ {label}
{ data = records || [] headers = Object.keys($backendUiStore.selectedModel.schema).filter( diff --git a/packages/builder/src/components/database/ModelDataTable/api.js b/packages/builder/src/components/database/ModelDataTable/api.js index cb98879567..f50290b091 100644 --- a/packages/builder/src/components/database/ModelDataTable/api.js +++ b/packages/builder/src/components/database/ModelDataTable/api.js @@ -1,7 +1,7 @@ import api from "builderStore/api" -export async function createUser(user, instanceId) { - const CREATE_USER_URL = `/api/${instanceId}/users` +export async function createUser(user) { + const CREATE_USER_URL = `/api/users` const response = await api.post(CREATE_USER_URL, user) return await response.json() } @@ -14,21 +14,21 @@ export async function createDatabase(appname, instanceName) { return await response.json() } -export async function deleteRecord(record, instanceId) { - const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}` +export async function deleteRecord(record) { + const DELETE_RECORDS_URL = `/api/${record._modelId}/records/${record._id}/${record._rev}` const response = await api.delete(DELETE_RECORDS_URL) return response } -export async function saveRecord(record, instanceId, modelId) { - const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records` +export async function saveRecord(record, modelId) { + const SAVE_RECORDS_URL = `/api/${modelId}/records` const response = await api.post(SAVE_RECORDS_URL, record) return await response.json() } -export async function fetchDataForView(viewName, instanceId) { - const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}` +export async function fetchDataForView(viewName) { + const FETCH_RECORDS_URL = `/api/views/${viewName}` const response = await api.get(FETCH_RECORDS_URL) return await response.json() diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte index 1f14858184..6e1a1d564b 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte @@ -18,7 +18,6 @@ let fieldToEdit $: modelFields = model.schema ? Object.entries(model.schema) : [] - $: instanceId = $backendUiStore.selectedDatabase._id function editField() {} @@ -27,7 +26,7 @@ function onFinishedFieldEdit() {} async function saveModel() { - const SAVE_MODEL_URL = `/api/${instanceId}/models` + const SAVE_MODEL_URL = `/api/models` const response = await api.post(SAVE_MODEL_URL, model) const newModel = await response.json() backendUiStore.actions.models.create(newModel) @@ -54,7 +53,10 @@
Fields -
(showFieldView = true)}> +
(showFieldView = true)}> Add new field
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte index 50283b8758..7e1eb5fd08 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte @@ -14,8 +14,6 @@ let errors = [] let selectedModel - $: instanceId = $backendUiStore.selectedDatabase._id - $: modelSchema = $backendUiStore.selectedModel ? Object.entries($backendUiStore.selectedModel.schema) : [] @@ -49,7 +47,6 @@ ...record, modelId: $backendUiStore.selectedModel._id, }, - instanceId, $backendUiStore.selectedModel._id ) if (recordResponse.errors) { diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte index de5542826e..c7d3ec8af0 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte @@ -29,7 +29,7 @@ function deleteView() {} async function saveView() { - const SAVE_VIEW_URL = `/api/${instanceId}/views` + const SAVE_VIEW_URL = `/api/views` const response = await api.post(SAVE_VIEW_URL, view) backendUiStore.update(state => { state.views = [...state.views, response.view] diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte index 7a4619b87d..eef38ecfec 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte @@ -10,12 +10,11 @@ let accessLevelId $: valid = username && password && accessLevelId - $: instanceId = $backendUiStore.selectedDatabase._id $: appId = $store.appId async function createUser() { const user = { name: username, username, password, accessLevelId } - const response = await api.createUser(user, instanceId) + const response = await api.createUser(user) backendUiStore.actions.users.create(response) onClosed() } @@ -29,15 +28,26 @@
- +
- +
- diff --git a/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte b/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte index 1d46aeb586..203267996b 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte @@ -6,7 +6,6 @@ export let record export let onClosed - $: instanceId = $backendUiStore.selectedDatabase._id
@@ -25,7 +24,7 @@ { - await api.deleteRecord(record, instanceId) + await api.deleteRecord(record) backendUiStore.actions.records.delete(record) onClosed() }}> diff --git a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte b/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte index 5c308a7abb..6d37246be9 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte @@ -32,6 +32,7 @@ {#if type === 'select'} {:else} 0} {checked} diff --git a/packages/builder/src/components/nav/HierarchyRow.svelte b/packages/builder/src/components/nav/HierarchyRow.svelte index 8921ab74de..7653e46fd0 100644 --- a/packages/builder/src/components/nav/HierarchyRow.svelte +++ b/packages/builder/src/components/nav/HierarchyRow.svelte @@ -7,7 +7,6 @@ CreateEditModelModal, CreateEditViewModal, } from "components/database/ModelDataTable/modals" - import api from "builderStore/api" const { open, close } = getContext("simple-modal") diff --git a/packages/builder/src/components/nav/SchemaManagementDrawer.svelte b/packages/builder/src/components/nav/SchemaManagementDrawer.svelte index 73aad483eb..483d6648fd 100644 --- a/packages/builder/src/components/nav/SchemaManagementDrawer.svelte +++ b/packages/builder/src/components/nav/SchemaManagementDrawer.svelte @@ -52,7 +52,7 @@ } async function deleteModel(modelToDelete) { - const DELETE_MODEL_URL = `/api/${instanceId}/models/${node._id}/${node._rev}` + const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}` const response = await api.delete(DELETE_MODEL_URL) backendUiStore.update(state => { state.models = state.models.filter( diff --git a/packages/builder/src/components/nav/UsersList.svelte b/packages/builder/src/components/nav/UsersList.svelte index 682c3e91e0..1a755eaccb 100644 --- a/packages/builder/src/components/nav/UsersList.svelte +++ b/packages/builder/src/components/nav/UsersList.svelte @@ -12,11 +12,10 @@ $: currentAppInfo = { appname: $store.appname, - instanceId: $backendUiStore.selectedDatabase._id, } async function fetchUsers() { - const FETCH_USERS_URL = `/api/${currentAppInfo.instanceId}/users` + const FETCH_USERS_URL = `/api/users` const response = await api.get(FETCH_USERS_URL) const users = await response.json() backendUiStore.update(state => { diff --git a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte index 00d7e6c171..523ed25031 100644 --- a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte @@ -91,7 +91,6 @@ ? screenPlaceholder : $store.currentPreviewItem, ], - appRootPath: "", } $: selectedComponentType = getComponentTypeName($store.currentComponentInfo) @@ -108,6 +107,8 @@ selectedComponentType, selectedComponentId, frontendDefinition, + appId: $store.appId, + instanceId: $backendUiStore.selectedDatabase._id, }) ) } diff --git a/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js b/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js index 3d305f1c1d..c9272dddb2 100644 --- a/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js +++ b/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js @@ -20,6 +20,7 @@ export default ` font-weight: bold; } + diff --git a/packages/builder/src/components/userInterface/CategoryTab.svelte b/packages/builder/src/components/userInterface/CategoryTab.svelte index d48e99fc71..fe9509bec9 100644 --- a/packages/builder/src/components/userInterface/CategoryTab.svelte +++ b/packages/builder/src/components/userInterface/CategoryTab.svelte @@ -7,6 +7,7 @@
{#each categories as category}
  • onClick(category)} class:active={selectedCategory === category}> {category.name} diff --git a/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte b/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte index c6acd8f18a..19d54523b2 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte +++ b/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte @@ -1,12 +1,14 @@ @@ -18,6 +20,6 @@ } -
    +
    \ No newline at end of file diff --git a/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte b/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte index 16f17ccc98..5989e6b3ed 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte +++ b/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte @@ -1,5 +1,7 @@ -
    +
    @@ -139,21 +243,38 @@
    - setHue(hue.detail)} /> + setHue(hue.detail)} on:dragend={dispatchValue} /> setAlpha(alpha.detail)} /> + on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)} + on:dragend={dispatchValue} + />
    + {#if !disableSwatches} +
    + {#if swatches.length > 0} + {#each swatches as color, idx} + applySwatch(color)} on:removeswatch={() => removeSwatch(idx)} /> + {/each} + {/if} + {#if swatches.length !== 12} +
    + + +
    + {/if} +
    + {/if} +
    - handleColorInput(event.target.value)} /> + handleColorInput(event.target.value)} on:change={dispatchInputChange} />
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte b/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte index 8f5bf64f70..32a5e63ff3 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte +++ b/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte @@ -2,11 +2,14 @@ import Colorpicker from "./Colorpicker.svelte" import CheckedBackground from "./CheckedBackground.svelte" import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte" + import {buildStyle} from "./helpers.js" import { fade } from 'svelte/transition'; import {getColorFormat} from "./utils.js" export let value = "#3ec1d3ff" + export let swatches = [] + export let disableSwatches = false export let open = false; export let width = "25px" export let height = "25px" @@ -17,8 +20,8 @@ let previewHeight = null let previewWidth = null - let pickerWidth = 250 - let pickerHeight = 300 + let pickerWidth = 0 + let pickerHeight = 0 let anchorEl = null let parentNodes = []; @@ -61,30 +64,33 @@ function openColorpicker(event) { if(colorPreview) { - const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect() - const {innerHeight, innerWidth} = window - - const {offsetLeft, offsetTop} = colorPreview - //get the scrollTop value for all scrollable parent elements - let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0); - - const spaceBelow = (innerHeight - spaceAbove) - previewHeight - const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop - - //TOO: Testing and Scroll Awareness for x Scroll - const spaceRight = (innerWidth - spaceLeft) + previewWidth - const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth - - dimensions = {top, left} - open = true; } } + $: if(open && colorPreview) { + const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect() + const {innerHeight, innerWidth} = window + + const {offsetLeft, offsetTop} = colorPreview + //get the scrollTop value for all scrollable parent elements + let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0); + + const spaceBelow = (innerHeight - spaceAbove) - previewHeight + const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop + + //TOO: Testing and Scroll Awareness for x Scroll + const spaceRight = (innerWidth - spaceLeft) + previewWidth + const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth + + dimensions = {top, left} + } + function onColorChange(color) { value = color.detail; dispatch("change", color.detail) } +
    @@ -94,8 +100,8 @@ {#if open} -
    - +
    +
    open = false} class="overlay">
    {/if} diff --git a/packages/builder/src/components/userInterface/Colorpicker/Input.svelte b/packages/builder/src/components/userInterface/Colorpicker/Input.svelte index 483fcd450d..811a51cea6 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/Input.svelte +++ b/packages/builder/src/components/userInterface/Colorpicker/Input.svelte @@ -24,5 +24,5 @@
    - +
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte b/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte index 47736d7b69..edf0888432 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte +++ b/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte @@ -10,7 +10,7 @@ let slider; let sliderWidth = 0; - function handleClick(mouseX) { + function onSliderChange(mouseX, isDrag = false) { const { left, width } = slider.getBoundingClientRect(); let clickPosition = mouseX - left; @@ -21,7 +21,8 @@ type === "hue" ? 360 * percentageClick : percentageClick; - dispatch("change", value); + + dispatch("change", {color: value, isDrag}); } } @@ -75,13 +76,14 @@
    handleClick(event.clientX)} + on:click={event => onSliderChange(event.clientX)} class="color-format-slider" class:hue={type === 'hue'} class:alpha={type === 'alpha'}>
    handleClick(e.detail)} + on:drag={e => onSliderChange(e.detail, true)} + on:dragend class="slider-thumb" {style} />
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte b/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte new file mode 100644 index 0000000000..e602b72d33 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte @@ -0,0 +1,53 @@ + + + + +
    + +
    hovered = true} on:mouseleave={() => hovered = false}> + {#if hovered} +
    dispatch("removeswatch")}> + dispatch("removeswatch")}>× +
    + {/if} +
    +
    +
    \ No newline at end of file diff --git a/packages/builder/src/components/userInterface/Colorpicker/drag.js b/packages/builder/src/components/userInterface/Colorpicker/drag.js index 0d7a9c2481..54fa5e924b 100644 --- a/packages/builder/src/components/userInterface/Colorpicker/drag.js +++ b/packages/builder/src/components/userInterface/Colorpicker/drag.js @@ -16,6 +16,7 @@ export default function(node) { function handleMouseUp() { window.removeEventListener("mousedown", handleMouseDown) window.removeEventListener("mousemove", handleMouseMove) + node.dispatchEvent(new CustomEvent("dragend")) } node.addEventListener("mousedown", handleMouseDown) diff --git a/packages/builder/src/components/userInterface/ItemTab/Item.svelte b/packages/builder/src/components/userInterface/ItemTab/Item.svelte index 0bf7f525df..f3751aeef7 100644 --- a/packages/builder/src/components/userInterface/ItemTab/Item.svelte +++ b/packages/builder/src/components/userInterface/ItemTab/Item.svelte @@ -3,7 +3,11 @@ export let item -
    +
    diff --git a/packages/builder/src/components/userInterface/PropertyControl.svelte b/packages/builder/src/components/userInterface/PropertyControl.svelte index b08371aeaf..0552c9bc65 100644 --- a/packages/builder/src/components/userInterface/PropertyControl.svelte +++ b/packages/builder/src/components/userInterface/PropertyControl.svelte @@ -38,7 +38,8 @@ {...handlevalueKey(value)} on:change={val => handleChange(key, val)} onChange={val => handleChange(key, val)} - {...props} /> + {...props} + name={key} />
    diff --git a/packages/builder/src/components/workflow/SetupPanel/DeleteWorkflowModal.svelte b/packages/builder/src/components/workflow/SetupPanel/DeleteWorkflowModal.svelte index 3ee07a107d..c84117b475 100644 --- a/packages/builder/src/components/workflow/SetupPanel/DeleteWorkflowModal.svelte +++ b/packages/builder/src/components/workflow/SetupPanel/DeleteWorkflowModal.svelte @@ -1,7 +1,6 @@ -
    -
    + +
    +
    diff --git a/packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte b/packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte index d966a5a569..97100b6626 100644 --- a/packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte +++ b/packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte @@ -1,7 +1,6 @@ diff --git a/packages/builder/src/components/workflow/WorkflowPanel/WorkflowPanel.svelte b/packages/builder/src/components/workflow/WorkflowPanel/WorkflowPanel.svelte index 442647761d..f5cd0c440d 100644 --- a/packages/builder/src/components/workflow/WorkflowPanel/WorkflowPanel.svelte +++ b/packages/builder/src/components/workflow/WorkflowPanel/WorkflowPanel.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" import { backendUiStore, workflowStore } from "builderStore" import { WorkflowList, BlockList } from "./" - import api from "builderStore/api" import blockDefinitions from "./blockDefinitions" let selectedTab = "WORKFLOWS" @@ -11,6 +10,7 @@
    (selectedTab = 'WORKFLOWS')}> @@ -18,6 +18,7 @@ {#if $workflowStore.currentWorkflow} (selectedTab = 'ADD')}> diff --git a/packages/builder/src/pages/[application]/workflow/[workflow]/_layout.svelte b/packages/builder/src/pages/[application]/workflow/[workflow]/_layout.svelte index f1b3bae003..a00f7b4ab0 100644 --- a/packages/builder/src/pages/[application]/workflow/[workflow]/_layout.svelte +++ b/packages/builder/src/pages/[application]/workflow/[workflow]/_layout.svelte @@ -1,3 +1,4 @@ diff --git a/packages/cli/src/commands/init/initHandler.js b/packages/cli/src/commands/init/initHandler.js index c0c4ad1b9e..3fe4134447 100644 --- a/packages/cli/src/commands/init/initHandler.js +++ b/packages/cli/src/commands/init/initHandler.js @@ -7,7 +7,7 @@ const Sqrl = require("squirrelly") const uuid = require("uuid") module.exports = opts => { - run(opts) + return run(opts) } const run = async opts => { diff --git a/packages/cli/src/commands/new/newHandler.js b/packages/cli/src/commands/new/newHandler.js index 44007acde7..64c974cdf9 100644 --- a/packages/cli/src/commands/new/newHandler.js +++ b/packages/cli/src/commands/new/newHandler.js @@ -17,9 +17,7 @@ const run = async opts => { exec(`cd ${join(opts.dir, opts.applicationId)} && npm install`) console.log(chalk.green(`Budibase app ${opts.name} created!`)) } catch (error) { - console.error( - chalk.red("Error creating new app", JSON.stringify(error, { space: 2 })) - ) + console.error(chalk.red("Error creating new app", error)) } } @@ -53,7 +51,9 @@ const createAppInstance = async opts => { const createInstCtx = { params: { clientId: process.env.CLIENT_ID, - applicationId: opts.applicationId, + }, + user: { + appId: opts.applicationId, }, request: { body: { name: `dev-${process.env.CLIENT_ID}` }, diff --git a/packages/cli/src/commands/run/runHandler.js b/packages/cli/src/commands/run/runHandler.js index 04b21a4ff0..e3f669cf53 100644 --- a/packages/cli/src/commands/run/runHandler.js +++ b/packages/cli/src/commands/run/runHandler.js @@ -5,7 +5,6 @@ module.exports = ({ dir }) => { dir = xPlatHomeDir(dir) process.env.BUDIBASE_DIR = resolve(dir) require("dotenv").config({ path: resolve(dir, ".env") }) - console.log("dotenv loaded") // dont make this a variable or top level require // ti will cause environment module to be loaded prematurely diff --git a/packages/client/package.json b/packages/client/package.json index 01b90a7bc5..acefff3770 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -15,7 +15,7 @@ "client": "web" } }, - "testURL": "http://jest-breaks-if-this-does-not-exist", + "testURL": "http://test.com", "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js", "\\.(css|less|sass|scss)$": "identity-obj-proxy" diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index f019897494..373ac1809e 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -1,9 +1,9 @@ import { authenticate } from "./authenticate" import { triggerWorkflow } from "./workflow" -export const createApi = ({ rootPath = "", setState, getState }) => { +export const createApi = ({ setState, getState }) => { const apiCall = method => async ({ url, body }) => { - const response = await fetch(`${rootPath}${url}`, { + const response = await fetch(url, { method: method, headers: { "Content-Type": "application/json", @@ -45,7 +45,6 @@ export const createApi = ({ rootPath = "", setState, getState }) => { const isSuccess = obj => !obj || !obj[ERROR_MEMBER] const apiOpts = { - rootPath, setState, getState, isSuccess, diff --git a/packages/client/src/createApp.js b/packages/client/src/createApp.js index a80c1009c7..9e1a9ea1b1 100644 --- a/packages/client/src/createApp.js +++ b/packages/client/src/createApp.js @@ -2,6 +2,7 @@ import { attachChildren } from "./render/attachChildren" import { createTreeNode } from "./render/prepareRenderComponent" import { screenRouter } from "./render/screenRouter" import { createStateManager } from "./state/stateManager" +import { getAppId } from "./render/getAppId" export const createApp = ({ componentLibraries, @@ -15,11 +16,9 @@ export const createApp = ({ const onScreenSlotRendered = screenSlotNode => { const onScreenSelected = (screen, url) => { const stateManager = createStateManager({ - frontendDefinition, componentLibraries, onScreenSlotRendered: () => {}, routeTo, - appRootPath: frontendDefinition.appRootPath, }) const getAttachChildrenParams = attachChildrenParams(stateManager) screenSlotNode.props._children = [screen.props] @@ -36,10 +35,10 @@ export const createApp = ({ routeTo = screenRouter({ screens: frontendDefinition.screens, onScreenSelected, - appRootPath: frontendDefinition.appRootPath, + window, }) const fallbackPath = window.location.pathname.replace( - frontendDefinition.appRootPath, + getAppId(window.document.cookie), "" ) routeTo(currentUrl || fallbackPath) @@ -59,10 +58,8 @@ export const createApp = ({ let rootTreeNode const pageStateManager = createStateManager({ - frontendDefinition, componentLibraries, onScreenSlotRendered, - appRootPath: frontendDefinition.appRootPath, // seems weird, but the routeTo variable may not be available at this point routeTo: url => routeTo(url), }) diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 95f722d445..87813a812d 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -1,5 +1,6 @@ import { createApp } from "./createApp" import { builtins, builtinLibName } from "./render/builtinComponents" +import { getAppId } from "./render/getAppId" /** * create a web application from static budibase definition files. @@ -8,7 +9,7 @@ import { builtins, builtinLibName } from "./render/builtinComponents" export const loadBudibase = async opts => { const _window = (opts && opts.window) || window // const _localStorage = (opts && opts.localStorage) || localStorage - + const appId = getAppId(_window.document.cookie) const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"] const user = {} @@ -20,9 +21,7 @@ export const loadBudibase = async opts => { for (let library of libraries) { // fetch the JavaScript for the component libraries from the server componentLibraryModules[library] = await import( - `/${frontendDefinition.appId}/componentlibrary?library=${encodeURI( - library - )}` + `/componentlibrary?library=${encodeURI(library)}` ) } @@ -38,11 +37,11 @@ export const loadBudibase = async opts => { componentLibraries: componentLibraryModules, frontendDefinition, user, - window, + window: _window, }) const route = _window.location - ? _window.location.pathname.replace(frontendDefinition.appRootPath, "") + ? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "") : "" initialisePage(frontendDefinition.page, _window.document.body, route) diff --git a/packages/client/src/render/getAppId.js b/packages/client/src/render/getAppId.js new file mode 100644 index 0000000000..6d3df8eac2 --- /dev/null +++ b/packages/client/src/render/getAppId.js @@ -0,0 +1,12 @@ +export const getAppId = docCookie => { + const cookie = + docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) || + docCookie.split(";").find(c => c.trim().startsWith("builder:token")) + + const base64Token = cookie.substring(lengthOfKey) + + const user = JSON.parse(atob(base64Token.split(".")[1])) + return user.appId +} + +const lengthOfKey = "budibase:token=".length diff --git a/packages/client/src/render/screenRouter.js b/packages/client/src/render/screenRouter.js index 64319102f8..fb0b005096 100644 --- a/packages/client/src/render/screenRouter.js +++ b/packages/client/src/render/screenRouter.js @@ -1,11 +1,20 @@ import regexparam from "regexparam" import { routerStore } from "../state/store" +import { getAppId } from "./getAppId" -export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => { +export const screenRouter = ({ screens, onScreenSelected, window }) => { const makeRootedPath = url => { - if (appRootPath) { - if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}` - return appRootPath + if ( + window.location && + (window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1") + ) { + const appId = getAppId(window.document.cookie) + if (url) { + if (url.startsWith(appId)) return url + return `/${appId}${url.startsWith("/") ? "" : "/"}${url}` + } + return appId } return url } @@ -70,7 +79,7 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => { ) return - const target = x.target || "_self" + const target = (x && x.target) || "_self" if (!y || target !== "_self" || x.host !== location.host) return e.preventDefault() diff --git a/packages/client/src/state/bbComponentApi.js b/packages/client/src/state/bbComponentApi.js index 7366a90f5c..112e3e8534 100644 --- a/packages/client/src/state/bbComponentApi.js +++ b/packages/client/src/state/bbComponentApi.js @@ -6,31 +6,19 @@ export const trimSlash = str => str.replace(/^\/+|\/+$/g, "") export const bbFactory = ({ store, - frontendDefinition, componentLibraries, onScreenSlotRendered, }) => { - const relativeUrl = url => { - if (!frontendDefinition.appRootPath) return url - if ( - url.startsWith("http:") || - url.startsWith("https:") || - url.startsWith("./") - ) - return url - - return frontendDefinition.appRootPath + "/" + trimSlash(url) - } - - const apiCall = method => (url, body) => - fetch(url, { + const apiCall = method => (url, body) => { + return fetch(url, { method: method, headers: { "Content-Type": "application/json", - "x-user-agent": "Budibase Builder", }, body: body && JSON.stringify(body), + credentials: "same-origin", }) + } const api = { post: apiCall("POST"), @@ -63,7 +51,6 @@ export const bbFactory = ({ getContext: getContext(treeNode), setContext: setContext(treeNode), store: store, - relativeUrl, api, parent, } diff --git a/packages/client/src/state/eventHandlers.js b/packages/client/src/state/eventHandlers.js index b013956dd4..cab825c24b 100644 --- a/packages/client/src/state/eventHandlers.js +++ b/packages/client/src/state/eventHandlers.js @@ -6,14 +6,13 @@ import { createApi } from "../api" export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" -export const eventHandlers = (rootPath, routeTo) => { +export const eventHandlers = routeTo => { const handler = (parameters, execute) => ({ execute, parameters, }) const api = createApi({ - rootPath, setState, getState: (path, fallback) => getState(path, fallback), }) diff --git a/packages/client/src/state/stateManager.js b/packages/client/src/state/stateManager.js index 8e777ca4b9..5c5f198956 100644 --- a/packages/client/src/state/stateManager.js +++ b/packages/client/src/state/stateManager.js @@ -21,13 +21,11 @@ const isMetaProp = propName => propName === "_styles" export const createStateManager = ({ - appRootPath, - frontendDefinition, componentLibraries, onScreenSlotRendered, routeTo, }) => { - let handlerTypes = eventHandlers(appRootPath, routeTo) + let handlerTypes = eventHandlers(routeTo) let currentState const getCurrentState = () => currentState @@ -35,7 +33,6 @@ export const createStateManager = ({ const bb = bbFactory({ store: appStore, getCurrentState, - frontendDefinition, componentLibraries, onScreenSlotRendered, }) diff --git a/packages/client/tests/screenRouting.spec.js b/packages/client/tests/screenRouting.spec.js index bb4380196f..3e4818961d 100644 --- a/packages/client/tests/screenRouting.spec.js +++ b/packages/client/tests/screenRouting.spec.js @@ -18,7 +18,7 @@ describe("screenRouting", () => { it("should load correct screen, for initial URL, when appRootPath is something", async () => { const { page, screens } = pageWith3Screens() - const { dom } = await load(page, screens, "/testApp/screen2", "/testApp") + const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1") const rootDiv = dom.window.document.body.children[0] expect(rootDiv.children.length).toBe(1) @@ -50,8 +50,8 @@ describe("screenRouting", () => { const { dom, app } = await load( page, screens, - "/testApp/screen2", - "/testApp" + "/TEST_APP_ID/screen2", + "127.0.0.1" ) app.routeTo()("/screen3") diff --git a/packages/client/tests/testAppDef.js b/packages/client/tests/testAppDef.js index c02c18a3b2..76bb3784b0 100644 --- a/packages/client/tests/testAppDef.js +++ b/packages/client/tests/testAppDef.js @@ -1,19 +1,33 @@ -import { JSDOM } from "jsdom" +import jsdom, { JSDOM } from "jsdom" import { loadBudibase } from "../src/index" -export const load = async (page, screens, url, appRootPath) => { +export const load = async (page, screens, url, host = "test.com") => { screens = screens || [] url = url || "/" - appRootPath = appRootPath || "" + + const fullUrl = `http://${host}${url}` + const cookieJar = new jsdom.CookieJar() + const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature` + cookieJar.setCookie( + `budibase:token=${cookie};domain=${host};path=/`, + fullUrl, + { + looseMode: false, + }, + () => {} + ) + const dom = new JSDOM("", { - url: `http://test${url}`, + url: fullUrl, + cookieJar, }) + autoAssignIds(page.props) for (let s of screens) { autoAssignIds(s.props) } setAppDef(dom.window, page, screens) - addWindowGlobals(dom.window, page, screens, appRootPath, { + addWindowGlobals(dom.window, page, screens, { hierarchy: {}, actions: [], triggers: [], @@ -27,11 +41,10 @@ export const load = async (page, screens, url, appRootPath) => { return { dom, app } } -const addWindowGlobals = (window, page, screens, appRootPath) => { +const addWindowGlobals = (window, page, screens) => { window["##BUDIBASE_FRONTEND_DEFINITION##"] = { page, screens, - appRootPath, } } @@ -88,7 +101,6 @@ const setAppDef = (window, page, screens) => { componentLibraries: [], page, screens, - appRootPath: "", } } diff --git a/packages/materialdesign-components/src/Test/createApp.js b/packages/materialdesign-components/src/Test/createApp.js index 96106635b7..0bd3098688 100644 --- a/packages/materialdesign-components/src/Test/createApp.js +++ b/packages/materialdesign-components/src/Test/createApp.js @@ -14,12 +14,6 @@ export default async () => { componentLibraries["@budibase/standard-components"] = standardcomponents const appDef = { hierarchy: {}, actions: {} } const user = { name: "yeo", permissions: [] } - const { initialisePage } = createApp( - componentLibraries, - { appRootPath: "" }, - appDef, - user, - {} - ) + const { initialisePage } = createApp(componentLibraries, {}, appDef, user, {}) return initialisePage } diff --git a/packages/server/.env.template b/packages/server/.env.template index 170d391520..f282d9c67f 100644 --- a/packages/server/.env.template +++ b/packages/server/.env.template @@ -5,9 +5,6 @@ COUCH_DB_URL={{couchDbUrl}} # identifies a client database - i.e. group of apps CLIENT_ID={{clientId}} -# Full access API key for server -ADMIN_SECRET={{adminSecret}} - # used to create cookie hashes JWT_SECRET={{cookieKey1}} diff --git a/packages/server/scripts/jestSetup.js b/packages/server/scripts/jestSetup.js index ab5bc83885..8fc7082554 100644 --- a/packages/server/scripts/jestSetup.js +++ b/packages/server/scripts/jestSetup.js @@ -4,5 +4,4 @@ process.env.NODE_ENV = "jest" process.env.JWT_SECRET = "test-jwtsecret" process.env.CLIENT_ID = "test-client-id" process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") -process.env.ADMIN_SECRET = "test-admin-secret" process.env.LOG_LEVEL = "silent" diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js index 9c9c7ff727..44b638ed41 100644 --- a/packages/server/src/api/controllers/accesslevel.js +++ b/packages/server/src/api/controllers/accesslevel.js @@ -8,7 +8,7 @@ const { } = require("../../utilities/accessLevels") exports.fetch = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const body = await db.query("database/by_type", { include_docs: true, key: ["accesslevel"], @@ -19,12 +19,12 @@ exports.fetch = async function(ctx) { { _id: ADMIN_LEVEL_ID, name: "Admin", - permissions: await generateAdminPermissions(ctx.params.instanceId), + permissions: await generateAdminPermissions(ctx.user.instanceId), }, { _id: POWERUSER_LEVEL_ID, name: "Power User", - permissions: await generatePowerUserPermissions(ctx.params.instanceId), + permissions: await generatePowerUserPermissions(ctx.user.instanceId), }, ] @@ -32,12 +32,12 @@ exports.fetch = async function(ctx) { } exports.find = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) ctx.body = await db.get(ctx.params.levelId) } exports.update = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const level = await db.get(ctx.params.levelId) level.name = ctx.body.name level.permissions = ctx.request.body.permissions @@ -48,7 +48,7 @@ exports.update = async function(ctx) { } exports.patch = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const level = await db.get(ctx.params.levelId) const { removedPermissions, addedPermissions, _rev } = ctx.request.body @@ -84,7 +84,7 @@ exports.patch = async function(ctx) { } exports.create = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const level = { name: ctx.request.body.name, @@ -101,7 +101,7 @@ exports.create = async function(ctx) { } exports.destroy = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) await db.remove(ctx.params.levelId, ctx.params.rev) ctx.message = `Access Level ${ctx.params.id} deleted successfully` ctx.status = 200 diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 0f604be0e7..43463fc4fd 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -1,6 +1,6 @@ const CouchDB = require("../../db") const ClientDb = require("../../db/clientDb") -const { getPackageForBuilder } = require("../../utilities/builder") +const { getPackageForBuilder, buildPage } = require("../../utilities/builder") const newid = require("../../db/newid") const env = require("../../environment") const instanceController = require("./instance") @@ -9,6 +9,7 @@ const { copy, exists, readFile, writeFile } = require("fs-extra") const { budibaseAppsDir } = require("../../utilities/budibaseDir") const { exec } = require("child_process") const sqrl = require("squirrelly") +const setBuilderToken = require("../../utilities/builder/setBuilderToken") exports.fetch = async function(ctx) { const db = new CouchDB(ClientDb.name(getClientId(ctx))) @@ -25,6 +26,14 @@ exports.fetchAppPackage = async function(ctx) { const db = new CouchDB(ClientDb.name(clientId)) const application = await db.get(ctx.params.applicationId) ctx.body = await getPackageForBuilder(ctx.config, application) + /* + instance is hardcoded now - this can only change when we move + pages and screens into the database + */ + const devInstance = application.instances.find( + i => i.name === `dev-${clientId}` + ) + setBuilderToken(ctx, ctx.params.applicationId, devInstance._id) } exports.create = async function(ctx) { @@ -37,10 +46,12 @@ exports.create = async function(ctx) { const appId = newid() // insert an appId -> clientId lookup const masterDb = new CouchDB("clientAppLookup") + await masterDb.put({ _id: appId, clientId, }) + const db = new CouchDB(ClientDb.name(clientId)) const newApplication = { @@ -56,18 +67,18 @@ exports.create = async function(ctx) { description: ctx.request.body.description, } - const { rev } = await db.post(newApplication) + const { rev } = await db.put(newApplication) newApplication._rev = rev - const createInstCtx = { - params: { - applicationId: newApplication._id, + user: { + appId: newApplication._id, }, request: { body: { name: `dev-${clientId}` }, }, } await instanceController.create(createInstCtx) + newApplication.instances.push(createInstCtx.body) if (ctx.isDev) { const newAppFolder = await createEmptyAppPackage(ctx, newApplication) @@ -100,15 +111,23 @@ const createEmptyAppPackage = async (ctx, app) => { await updateJsonFile(join(appsFolder, app._id, "package.json"), { name: npmFriendlyAppName(app.name), }) - await updateJsonFile( + + const mainJson = await updateJsonFile( join(appsFolder, app._id, "pages", "main", "page.json"), app ) - await updateJsonFile( + + await buildPage(ctx.config, app._id, "main", { page: mainJson }) + + const unauthenticatedJson = await updateJsonFile( join(appsFolder, app._id, "pages", "unauthenticated", "page.json"), app ) + await buildPage(ctx.config, app._id, "unauthenticated", { + page: unauthenticatedJson, + }) + return newAppFolder } @@ -134,6 +153,7 @@ const updateJsonFile = async (filePath, app) => { const json = await readFile(filePath, "utf8") const newJson = sqrl.Render(json, app) await writeFile(filePath, newJson, "utf8") + return JSON.parse(newJson) } const runNpmInstall = async newAppFolder => { diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 5ec3aea617..e9570e270f 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -4,25 +4,31 @@ const ClientDb = require("../../db/clientDb") const bcrypt = require("../../utilities/bcrypt") exports.authenticate = async ctx => { + if (!ctx.user.appId) ctx.throw(400, "No appId") + const { username, password } = ctx.request.body if (!username) ctx.throw(400, "Username Required.") if (!password) ctx.throw(400, "Password Required") const masterDb = new CouchDB("clientAppLookup") - const { clientId } = await masterDb.get(ctx.params.appId) + + const { clientId } = await masterDb.get(ctx.user.appId) if (!clientId) { ctx.throw(400, "ClientId not suplied") } // find the instance that the user is associated with const db = new CouchDB(ClientDb.name(clientId)) - const appId = ctx.params.appId - const app = await db.get(appId) + const app = await db.get(ctx.user.appId) const instanceId = app.userInstanceMap[username] if (!instanceId) - ctx.throw(500, "User is not associated with an instance of app", appId) + ctx.throw( + 500, + "User is not associated with an instance of app", + ctx.user.appId + ) // Check the user exists in the instance DB by username const instanceDb = new CouchDB(instanceId) @@ -41,16 +47,22 @@ exports.authenticate = async ctx => { const payload = { userId: dbUser._id, accessLevelId: dbUser.accessLevelId, - instanceId: instanceId, + appId: ctx.user.appId, + instanceId, } const token = jwt.sign(payload, ctx.config.jwtSecret, { expiresIn: "1 day", }) - const ONE_DAY_FROM_NOW = new Date(Date.now() + 24 * 3600) + const expires = new Date() + expires.setDate(expires.getDate() + 1) - ctx.cookies.set("budibase:token", token, { expires: ONE_DAY_FROM_NOW }) + ctx.cookies.set("budibase:token", token, { + expires, + path: "/", + httpOnly: false, + }) ctx.body = { token, diff --git a/packages/server/src/api/controllers/instance.js b/packages/server/src/api/controllers/instance.js index 155437371f..903d3ff651 100644 --- a/packages/server/src/api/controllers/instance.js +++ b/packages/server/src/api/controllers/instance.js @@ -4,19 +4,19 @@ const newid = require("../../db/newid") exports.create = async function(ctx) { const instanceName = ctx.request.body.name - const appShortId = ctx.params.applicationId.substring(0, 7) + const { appId } = ctx.user + const appShortId = appId.substring(0, 7) const instanceId = `inst_${appShortId}_${newid()}` - const { applicationId } = ctx.params const masterDb = new CouchDB("clientAppLookup") - const { clientId } = await masterDb.get(applicationId) + const { clientId } = await masterDb.get(appId) const db = new CouchDB(instanceId) await db.put({ _id: "_design/database", metadata: { clientId, - applicationId, + applicationId: appId, }, views: { by_username: { @@ -46,7 +46,7 @@ exports.create = async function(ctx) { // Add the new instance under the app clientDB const clientDb = new CouchDB(client.name(clientId)) - const budibaseApp = await clientDb.get(applicationId) + const budibaseApp = await clientDb.get(appId) const instance = { _id: instanceId, name: instanceName } budibaseApp.instances.push(instance) await clientDb.put(budibaseApp) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 650342b33c..e7792195cb 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -2,7 +2,7 @@ const CouchDB = require("../../db") const newid = require("../../db/newid") exports.fetch = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const body = await db.query("database/by_type", { include_docs: true, key: ["model"], @@ -11,13 +11,13 @@ exports.fetch = async function(ctx) { } exports.find = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const model = await db.get(ctx.params.id) ctx.body = model } exports.create = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const newModel = { type: "model", ...ctx.request.body, @@ -65,7 +65,7 @@ exports.create = async function(ctx) { exports.update = async function() {} exports.destroy = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const modelToDelete = await db.get(ctx.params.modelId) diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index cf8eb27606..4e4af81e01 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -3,7 +3,7 @@ const validateJs = require("validate.js") const newid = require("../../db/newid") exports.save = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const record = ctx.request.body record.modelId = ctx.params.modelId @@ -46,7 +46,7 @@ exports.save = async function(ctx) { ctx.eventEmitter && ctx.eventEmitter.emit(`record:save`, { record, - instanceId: ctx.params.instanceId, + instanceId: ctx.user.instanceId, }) ctx.body = record ctx.status = 200 @@ -54,7 +54,7 @@ exports.save = async function(ctx) { } exports.fetchView = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const response = await db.query(`database/${ctx.params.viewName}`, { include_docs: true, }) @@ -62,7 +62,7 @@ exports.fetchView = async function(ctx) { } exports.fetchModelRecords = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const response = await db.query(`database/all_${ctx.params.modelId}`, { include_docs: true, }) @@ -70,7 +70,7 @@ exports.fetchModelRecords = async function(ctx) { } exports.search = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const response = await db.allDocs({ include_docs: true, ...ctx.request.body, @@ -79,7 +79,7 @@ exports.search = async function(ctx) { } exports.find = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const record = await db.get(ctx.params.recordId) if (record.modelId !== ctx.params.modelId) { ctx.throw(400, "Supplied modelId doe not match the record's modelId") @@ -89,7 +89,7 @@ exports.find = async function(ctx) { } exports.destroy = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const record = await db.get(ctx.params.recordId) if (record.modelId !== ctx.params.modelId) { ctx.throw(400, "Supplied modelId doe not match the record's modelId") @@ -101,7 +101,7 @@ exports.destroy = async function(ctx) { exports.validate = async function(ctx) { const errors = await validate({ - instanceId: ctx.params.instanceId, + instanceId: ctx.user.instanceId, modelId: ctx.params.modelId, record: ctx.request.body, }) diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js index 557a9efe7a..67ec8e391a 100644 --- a/packages/server/src/api/controllers/static.js +++ b/packages/server/src/api/controllers/static.js @@ -4,11 +4,15 @@ const { budibaseAppsDir, budibaseTempDir, } = require("../../utilities/budibaseDir") -const env = require("../../environment") +const setBuilderToken = require("../../utilities/builder/setBuilderToken") +const { ANON_LEVEL_ID } = require("../../utilities/accessLevels") +const jwt = require("jsonwebtoken") exports.serveBuilder = async function(ctx) { let builderPath = resolve(__dirname, "../../../builder") - ctx.cookies.set("builder:token", env.ADMIN_SECRET) + if (ctx.file === "index.html") { + setBuilderToken(ctx) + } await send(ctx, ctx.file, { root: ctx.devPath || builderPath }) } @@ -20,6 +24,33 @@ exports.serveApp = async function(ctx) { "public", ctx.isAuthenticated ? "main" : "unauthenticated" ) + // only set the appId cookie for /appId .. we COULD check for valid appIds + // but would like to avoid that DB hit + const looksLikeAppId = /^[0-9a-f]{32}$/.test(ctx.params.appId) + if (looksLikeAppId && !ctx.isAuthenticated) { + const anonUser = { + userId: "ANON", + accessLevelId: ANON_LEVEL_ID, + appId: ctx.params.appId, + } + const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret) + ctx.cookies.set("budibase:token", anonToken, { + path: "/", + httpOnly: false, + }) + } + + await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath }) +} + +exports.serveAppAsset = async function(ctx) { + // default to homedir + const appPath = resolve( + budibaseAppsDir(), + ctx.user.appId, + "public", + ctx.isAuthenticated ? "main" : "unauthenticated" + ) await send(ctx, ctx.file, { root: ctx.devPath || appPath }) } @@ -28,7 +59,7 @@ exports.serveComponentLibrary = async function(ctx) { // default to homedir let componentLibraryPath = resolve( budibaseAppsDir(), - ctx.params.appId, + ctx.user.appId, "node_modules", decodeURI(ctx.query.library), "dist" diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 1b66cf1cca..71816f1c78 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -8,7 +8,7 @@ const { } = require("../../utilities/accessLevels") exports.fetch = async function(ctx) { - const database = new CouchDB(ctx.params.instanceId) + const database = new CouchDB(ctx.user.instanceId) const data = await database.query("database/by_type", { include_docs: true, key: ["user"], @@ -18,7 +18,7 @@ exports.fetch = async function(ctx) { } exports.create = async function(ctx) { - const database = new CouchDB(ctx.params.instanceId) + const database = new CouchDB(ctx.user.instanceId) const appId = (await database.get("_design/database")).metadata.applicationId const { username, password, name, accessLevelId } = ctx.request.body @@ -50,7 +50,7 @@ exports.create = async function(ctx) { app.userInstanceMap = { ...app.userInstanceMap, - [username]: ctx.params.instanceId, + [username]: ctx.user.instanceId, } await db.put(app) @@ -66,14 +66,14 @@ exports.create = async function(ctx) { exports.update = async function() {} exports.destroy = async function(ctx) { - const database = new CouchDB(ctx.params.instanceId) + const database = new CouchDB(ctx.user.instanceId) await database.destroy(getUserId(ctx.params.username)) ctx.message = `User ${ctx.params.username} deleted.` ctx.status = 200 } exports.find = async function(ctx) { - const database = new CouchDB(ctx.params.instanceId) + const database = new CouchDB(ctx.user.instanceId) const user = await database.get(getUserId(ctx.params.username)) ctx.body = { username: user.username, diff --git a/packages/server/src/api/controllers/view.js b/packages/server/src/api/controllers/view.js index 9e847da358..d449ae36f9 100644 --- a/packages/server/src/api/controllers/view.js +++ b/packages/server/src/api/controllers/view.js @@ -3,7 +3,7 @@ const CouchDB = require("../../db") const controller = { query: async () => {}, fetch: async ctx => { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const designDoc = await db.get("_design/database") const response = [] @@ -24,7 +24,7 @@ const controller = { ctx.body = response }, create: async ctx => { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const newView = ctx.request.body const designDoc = await db.get("_design/database") @@ -38,7 +38,7 @@ const controller = { ctx.message = `View ${newView.name} created successfully.` }, destroy: async ctx => { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) ctx.body = await db.destroy(ctx.params.userId) }, } diff --git a/packages/server/src/api/controllers/workflow/index.js b/packages/server/src/api/controllers/workflow/index.js index f1dd1bcb89..5c77bbfaa6 100644 --- a/packages/server/src/api/controllers/workflow/index.js +++ b/packages/server/src/api/controllers/workflow/index.js @@ -2,7 +2,7 @@ const CouchDB = require("../../../db") const newid = require("../../../db/newid") exports.create = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const workflow = ctx.request.body workflow._id = newid() @@ -22,7 +22,7 @@ exports.create = async function(ctx) { } exports.update = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const workflow = ctx.request.body const response = await db.put(workflow) @@ -40,7 +40,7 @@ exports.update = async function(ctx) { } exports.fetch = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) const response = await db.query(`database/by_type`, { key: ["workflow"], include_docs: true, @@ -69,6 +69,6 @@ exports.fetchActionScript = async function(ctx) { } exports.destroy = async function(ctx) { - const db = new CouchDB(ctx.params.instanceId) + const db = new CouchDB(ctx.user.instanceId) ctx.body = await db.remove(ctx.params.id, ctx.params.rev) } diff --git a/packages/server/src/api/routes/accesslevel.js b/packages/server/src/api/routes/accesslevel.js index d34acab02c..af1ff80ec6 100644 --- a/packages/server/src/api/routes/accesslevel.js +++ b/packages/server/src/api/routes/accesslevel.js @@ -4,11 +4,11 @@ const controller = require("../controllers/accesslevel") const router = Router() router - .post("/api/:instanceId/accesslevels", controller.create) - .put("/api/:instanceId/accesslevels", controller.update) - .get("/api/:instanceId/accesslevels", controller.fetch) - .get("/api/:instanceId/accesslevels/:levelId", controller.find) - .delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy) - .patch("/api/:instanceId/accesslevels/:levelId", controller.patch) + .post("/api/accesslevels", controller.create) + .put("/api/accesslevels", controller.update) + .get("/api/accesslevels", controller.fetch) + .get("/api/accesslevels/:levelId", controller.find) + .delete("/api/accesslevels/:levelId/:rev", controller.destroy) + .patch("/api/accesslevels/:levelId", controller.patch) module.exports = router diff --git a/packages/server/src/api/routes/auth.js b/packages/server/src/api/routes/auth.js index fa95a3a5e6..b4b68e8929 100644 --- a/packages/server/src/api/routes/auth.js +++ b/packages/server/src/api/routes/auth.js @@ -3,6 +3,6 @@ const controller = require("../controllers/auth") const router = Router() -router.post("/:appId/api/authenticate", controller.authenticate) +router.post("/api/authenticate", controller.authenticate) module.exports = router diff --git a/packages/server/src/api/routes/instance.js b/packages/server/src/api/routes/instance.js index 9b7b3db511..00fce6bb12 100644 --- a/packages/server/src/api/routes/instance.js +++ b/packages/server/src/api/routes/instance.js @@ -6,7 +6,7 @@ const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .post("/api/:applicationId/instances", authorized(BUILDER), controller.create) + .post("/api/instances", authorized(BUILDER), controller.create) .delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy) module.exports = router diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js index f1ec46dbe5..10882aa057 100644 --- a/packages/server/src/api/routes/model.js +++ b/packages/server/src/api/routes/model.js @@ -1,17 +1,21 @@ const Router = require("@koa/router") const modelController = require("../controllers/model") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch) - .get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find) - .post("/api/:instanceId/models", authorized(BUILDER), modelController.create) + .get("/api/models", authorized(BUILDER), modelController.fetch) + .get( + "/api/models/:id", + authorized(READ_MODEL, ctx => ctx.params.id), + modelController.find + ) + .post("/api/models", authorized(BUILDER), modelController.create) // .patch("/api/:instanceId/models", controller.update) .delete( - "/api/:instanceId/models/:modelId/:revId", + "/api/models/:modelId/:revId", authorized(BUILDER), modelController.destroy ) diff --git a/packages/server/src/api/routes/record.js b/packages/server/src/api/routes/record.js index d555d3d8c8..deba513aa9 100644 --- a/packages/server/src/api/routes/record.js +++ b/packages/server/src/api/routes/record.js @@ -7,28 +7,28 @@ const router = Router() router .get( - "/api/:instanceId/:modelId/records", + "/api/:modelId/records", authorized(READ_MODEL, ctx => ctx.params.modelId), recordController.fetchModelRecords ) .get( - "/api/:instanceId/:modelId/records/:recordId", + "/api/:modelId/records/:recordId", authorized(READ_MODEL, ctx => ctx.params.modelId), recordController.find ) - .post("/api/:instanceId/records/search", recordController.search) + .post("/api/records/search", recordController.search) .post( - "/api/:instanceId/:modelId/records", + "/api/:modelId/records", authorized(WRITE_MODEL, ctx => ctx.params.modelId), recordController.save ) .post( - "/api/:instanceId/:modelId/records/validate", + "/api/:modelId/records/validate", authorized(WRITE_MODEL, ctx => ctx.params.modelId), recordController.validate ) .delete( - "/api/:instanceId/:modelId/records/:recordId/:revId", + "/api/:modelId/records/:recordId/:revId", authorized(WRITE_MODEL, ctx => ctx.params.modelId), recordController.destroy ) diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 19823aab68..60e29bf363 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -6,12 +6,8 @@ const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch) - .post("/api/:instanceId/screens", authorized(BUILDER), controller.save) - .delete( - "/api/:instanceId/:screenId/:revId", - authorized(BUILDER), - controller.destroy - ) + .get("/api/screens", authorized(BUILDER), controller.fetch) + .post("/api/screens", authorized(BUILDER), controller.save) + .delete("/api/:screenId/:revId", authorized(BUILDER), controller.destroy) module.exports = router diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 5909d31d09..6fff1996fa 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -21,7 +21,8 @@ if (env.NODE_ENV !== "production") { } router - .get("/:appId/componentlibrary", controller.serveComponentLibrary) - .get("/:appId/:file*", controller.serveApp) + .get("/componentlibrary", controller.serveComponentLibrary) + .get("/assets/:file*", controller.serveAppAsset) + .get("/:appId/:path*", controller.serveApp) module.exports = router diff --git a/packages/server/src/api/routes/tests/accesslevel.spec.js b/packages/server/src/api/routes/tests/accesslevel.spec.js index ef2d1575cd..81ab6a78ad 100644 --- a/packages/server/src/api/routes/tests/accesslevel.spec.js +++ b/packages/server/src/api/routes/tests/accesslevel.spec.js @@ -36,17 +36,17 @@ describe("/accesslevels", () => { beforeEach(async () => { instanceId = (await createInstance(request, appId))._id - model = await createModel(request, instanceId) - view = await createView(request, instanceId) + model = await createModel(request, appId, instanceId) + view = await createView(request, appId, instanceId) }) describe("create", () => { it("returns a success message when level is successfully created", async () => { const res = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "user" }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) @@ -62,17 +62,17 @@ describe("/accesslevels", () => { it("should list custom levels, plus 2 default levels", async () => { const createRes = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const customLevel = createRes.body const res = await request - .get(`/api/${instanceId}/accesslevels`) - .set(defaultHeaders) + .get(`/api/accesslevels`) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) @@ -95,22 +95,22 @@ describe("/accesslevels", () => { describe("destroy", () => { it("should delete custom access level", async () => { const createRes = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const customLevel = createRes.body await request - .delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`) - .set(defaultHeaders) + .delete(`/api/accesslevels/${customLevel._id}/${customLevel._rev}`) + .set(defaultHeaders(appId, instanceId)) .expect(200) await request - .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) - .set(defaultHeaders) + .get(`/api/accesslevels/${customLevel._id}`) + .set(defaultHeaders(appId, instanceId)) .expect(404) }) }) @@ -118,27 +118,27 @@ describe("/accesslevels", () => { describe("patch", () => { it("should add given permissions", async () => { const createRes = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const customLevel = createRes.body await request - .patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .patch(`/api/accesslevels/${customLevel._id}`) .send({ _rev: customLevel._rev, addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const finalRes = await request - .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) - .set(defaultHeaders) + .get(`/api/accesslevels/${customLevel._id}`) + .set(defaultHeaders(appId, instanceId)) .expect(200) expect(finalRes.body.permissions.length).toBe(2) @@ -148,7 +148,7 @@ describe("/accesslevels", () => { it("should remove given permissions", async () => { const createRes = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "user", permissions: [ @@ -156,25 +156,25 @@ describe("/accesslevels", () => { { itemId: model._id, name: WRITE_MODEL }, ] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const customLevel = createRes.body await request - .patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .patch(`/api/accesslevels/${customLevel._id}`) .send({ _rev: customLevel._rev, removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }] }) - .set(defaultHeaders) + .set(defaultHeaders(appId, instanceId)) .expect('Content-Type', /json/) .expect(200) const finalRes = await request - .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) - .set(defaultHeaders) + .get(`/api/accesslevels/${customLevel._id}`) + .set(defaultHeaders(appId, instanceId)) .expect(200) expect(finalRes.body.permissions.length).toBe(1) diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 7f64419778..d22ab02016 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -34,7 +34,7 @@ describe("/applications", () => { const res = await request .post("/api/applications") .send({ name: "My App" }) - .set(defaultHeaders) + .set(defaultHeaders()) .expect('Content-Type', /json/) .expect(200) expect(res.res.statusMessage).toEqual("Application My App created successfully") @@ -49,6 +49,7 @@ describe("/applications", () => { method: "POST", url: `/api/applications`, instanceId: instance._id, + appId: otherApplication._id, body: { name: "My App" } }) }) @@ -63,7 +64,7 @@ describe("/applications", () => { const res = await request .get("/api/applications") - .set(defaultHeaders) + .set(defaultHeaders()) .expect('Content-Type', /json/) .expect(200) @@ -77,13 +78,13 @@ describe("/applications", () => { const blah = await request .post("/api/applications") .send({ name: "app2", clientId: "new_client"}) - .set(defaultHeaders) + .set(defaultHeaders()) .expect('Content-Type', /json/) //.expect(200) const client1Res = await request .get(`/api/applications?clientId=${TEST_CLIENT_ID}`) - .set(defaultHeaders) + .set(defaultHeaders()) .expect('Content-Type', /json/) .expect(200) @@ -92,7 +93,7 @@ describe("/applications", () => { const client2Res = await request .get(`/api/applications?clientId=new_client`) - .set(defaultHeaders) + .set(defaultHeaders()) .expect('Content-Type', /json/) .expect(200) @@ -109,6 +110,7 @@ describe("/applications", () => { method: "GET", url: `/api/applications`, instanceId: instance._id, + appId: otherApplication._id, }) }) }) diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 495b841b10..17f096ec6b 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -4,8 +4,12 @@ const supertest = require("supertest") const app = require("../../../app") const { POWERUSER_LEVEL_ID, + ANON_LEVEL_ID, + BUILDER_LEVEL_ID, generateAdminPermissions, } = require("../../../utilities/accessLevels") +const jwt = require("jsonwebtoken") +const env = require("../../../environment") const TEST_CLIENT_ID = "test-client-id" @@ -20,13 +24,23 @@ exports.supertest = async () => { return { request, server } } -exports.defaultHeaders = { - Accept: "application/json", - Cookie: ["builder:token=test-admin-secret"], - "x-user-agent": "Budibase Builder", +exports.defaultHeaders = (appId, instanceId) => { + const builderUser = { + userId: "BUILDER", + accessLevelId: BUILDER_LEVEL_ID, + appId, + instanceId, + } + + const builderToken = jwt.sign(builderUser, env.JWT_SECRET) + + return { + Accept: "application/json", + Cookie: [`builder:token=${builderToken}`], + } } -exports.createModel = async (request, instanceId, model) => { +exports.createModel = async (request, appId, instanceId, model) => { model = model || { name: "TestModel", type: "model", @@ -42,20 +56,20 @@ exports.createModel = async (request, instanceId, model) => { } const res = await request - .post(`/api/${instanceId}/models`) - .set(exports.defaultHeaders) + .post(`/api/models`) + .set(exports.defaultHeaders(appId, instanceId)) .send(model) return res.body } -exports.createView = async (request, instanceId, view) => { +exports.createView = async (request, appId, instanceId, view) => { view = view || { map: "function(doc) { emit(doc[doc.key], doc._id); } ", } const res = await request - .post(`/api/${instanceId}/views`) - .set(exports.defaultHeaders) + .post(`/api/views`) + .set(exports.defaultHeaders(appId, instanceId)) .send(view) return res.body } @@ -65,10 +79,10 @@ exports.createClientDatabase = async id => await create(id || TEST_CLIENT_ID) exports.createApplication = async (request, name = "test_application") => { const res = await request .post("/api/applications") - .set(exports.defaultHeaders) .send({ name, }) + .set(exports.defaultHeaders()) return res.body } @@ -76,23 +90,24 @@ exports.destroyClientDatabase = async () => await destroy(TEST_CLIENT_ID) exports.createInstance = async (request, appId) => { const res = await request - .post(`/api/${appId}/instances`) - .set(exports.defaultHeaders) + .post(`/api/instances`) .send({ - name: "test-instance", + name: "test-instance2", }) + .set(exports.defaultHeaders(appId)) return res.body } exports.createUser = async ( request, + appId, instanceId, username = "babs", password = "babs_password" ) => { const res = await request - .post(`/api/${instanceId}/users`) - .set(exports.defaultHeaders) + .post(`/api/users`) + .set(exports.defaultHeaders(appId, instanceId)) .send({ name: "Bill", username, @@ -104,6 +119,7 @@ exports.createUser = async ( const createUserWithOnePermission = async ( request, + appId, instanceId, permName, itemId @@ -115,17 +131,19 @@ const createUserWithOnePermission = async ( return await createUserWithPermissions( request, + appId, instanceId, permissions, "onePermOnlyUser" ) } -const createUserWithAdminPermissions = async (request, instanceId) => { +const createUserWithAdminPermissions = async (request, appId, instanceId) => { let permissions = await generateAdminPermissions(instanceId) return await createUserWithPermissions( request, + appId, instanceId, permissions, "adminUser" @@ -134,6 +152,7 @@ const createUserWithAdminPermissions = async (request, instanceId) => { const createUserWithAllPermissionExceptOne = async ( request, + appId, instanceId, permName, itemId @@ -145,6 +164,7 @@ const createUserWithAllPermissionExceptOne = async ( return await createUserWithPermissions( request, + appId, instanceId, permissions, "allPermsExceptOneUser" @@ -153,19 +173,20 @@ const createUserWithAllPermissionExceptOne = async ( const createUserWithPermissions = async ( request, + appId, instanceId, permissions, username ) => { const accessRes = await request - .post(`/api/${instanceId}/accesslevels`) + .post(`/api/accesslevels`) .send({ name: "TestLevel", permissions }) - .set(exports.defaultHeaders) + .set(exports.defaultHeaders(appId, instanceId)) const password = `password_${username}` await request - .post(`/api/${instanceId}/users`) - .set(exports.defaultHeaders) + .post(`/api/users`) + .set(exports.defaultHeaders(appId, instanceId)) .send({ name: username, username, @@ -173,11 +194,20 @@ const createUserWithPermissions = async ( accessLevelId: accessRes.body._id, }) - const db = new CouchDB(instanceId) - const designDoc = await db.get("_design/database") + //const db = new CouchDB(instanceId) + //const designDoc = await db.get("_design/database") + + const anonUser = { + userId: "ANON", + accessLevelId: ANON_LEVEL_ID, + appId: appId, + } + + const anonToken = jwt.sign(anonUser, env.JWT_SECRET) const loginResult = await request - .post(`/${designDoc.metadata.applicationId}/api/authenticate`) + .post(`/api/authenticate`) + .set({ Cookie: `budibase:token=${anonToken}` }) .send({ username, password }) // returning necessary request headers @@ -192,12 +222,14 @@ exports.testPermissionsForEndpoint = async ({ method, url, body, + appId, instanceId, permissionName, itemId, }) => { const headers = await createUserWithOnePermission( request, + appId, instanceId, permissionName, itemId @@ -209,6 +241,7 @@ exports.testPermissionsForEndpoint = async ({ const noPermsHeaders = await createUserWithAllPermissionExceptOne( request, + appId, instanceId, permissionName, itemId @@ -224,9 +257,14 @@ exports.builderEndpointShouldBlockNormalUsers = async ({ method, url, body, + appId, instanceId, }) => { - const headers = await createUserWithAdminPermissions(request, instanceId) + const headers = await createUserWithAdminPermissions( + request, + appId, + instanceId + ) await createRequest(request, method, url, body) .set(headers) diff --git a/packages/server/src/api/routes/tests/instance.spec.js b/packages/server/src/api/routes/tests/instance.spec.js index 0b8bd44cb6..3bbed4d8e6 100644 --- a/packages/server/src/api/routes/tests/instance.spec.js +++ b/packages/server/src/api/routes/tests/instance.spec.js @@ -24,9 +24,9 @@ describe("/instances", () => { it("returns a success message when the instance database is successfully created", async () => { const res = await request - .post(`/api/${TEST_APP_ID}/instances`) + .post(`/api/instances`) .send({ name: "test-instance" }) - .set(defaultHeaders) + .set(defaultHeaders(TEST_APP_ID)) .expect('Content-Type', /json/) .expect(200) @@ -42,7 +42,7 @@ describe("/instances", () => { const instance = await createInstance(request, TEST_APP_ID); const res = await request .delete(`/api/instances/${instance._id}`) - .set(defaultHeaders) + .set(defaultHeaders(TEST_APP_ID)) .expect(200) expect(res.res.statusMessage).toEqual(`Instance Database ${instance._id} successfully destroyed.`); diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index 7134245fb3..4d4e7aeb08 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -33,7 +33,7 @@ describe("/models", () => { it("returns a success message when the model is successfully created", done => { request - .post(`/api/${instance._id}/models`) + .post(`/api/models`) .send({ name: "TestModel", key: "name", @@ -41,7 +41,7 @@ describe("/models", () => { name: { type: "string" } } }) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) .end(async (err, res) => { @@ -55,8 +55,9 @@ describe("/models", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "POST", - url: `/api/${instance._id}/models`, + url: `/api/models`, instanceId: instance._id, + appId: app._id, body: { name: "TestModel", key: "name", @@ -73,13 +74,13 @@ describe("/models", () => { beforeEach(async () => { instance = await createInstance(request, app._id) - testModel = await createModel(request, instance._id, testModel) + testModel = await createModel(request, app._id, instance._id, testModel) }); it("returns all the models for that instance in the response body", done => { request - .get(`/api/${instance._id}/models`) - .set(defaultHeaders) + .get(`/api/models`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) .end(async (_, res) => { @@ -94,8 +95,9 @@ describe("/models", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "GET", - url: `/api/${instance._id}/models`, + url: `/api/models`, instanceId: instance._id, + appId: app._id, }) }) }); @@ -105,7 +107,7 @@ describe("/models", () => { beforeEach(async () => { instance = await createInstance(request, app._id) - testModel = await createModel(request, instance._id, testModel) + testModel = await createModel(request, app._id, instance._id, testModel) }); afterEach(() => { @@ -114,8 +116,8 @@ describe("/models", () => { it("returns a success response when a model is deleted.", async done => { request - .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) - .set(defaultHeaders) + .delete(`/api/models/${testModel._id}/${testModel._rev}`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) .end(async (_, res) => { @@ -125,7 +127,7 @@ describe("/models", () => { }) it("deletes linked references to the model after deletion", async done => { - const linkedModel = await createModel(request, instance._id, { + const linkedModel = await createModel(request, app._id, instance._id, { name: "LinkedModel", type: "model", key: "name", @@ -147,8 +149,8 @@ describe("/models", () => { }) request - .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) - .set(defaultHeaders) + .delete(`/api/models/${testModel._id}/${testModel._rev}`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) .end(async (_, res) => { @@ -163,8 +165,9 @@ describe("/models", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "DELETE", - url: `/api/${instance._id}/models/${testModel._id}/${testModel._rev}`, + url: `/api/models/${testModel._id}/${testModel._rev}`, instanceId: instance._id, + appId: app._id, }) }) diff --git a/packages/server/src/api/routes/tests/record.spec.js b/packages/server/src/api/routes/tests/record.spec.js index 22ac67ecdc..62cf205b1c 100644 --- a/packages/server/src/api/routes/tests/record.spec.js +++ b/packages/server/src/api/routes/tests/record.spec.js @@ -27,7 +27,7 @@ describe("/records", () => { beforeEach(async () => { instance = await createInstance(request, app._id) - model = await createModel(request, instance._id) + model = await createModel(request, app._id, instance._id) record = { name: "Test Contact", status: "new", @@ -39,9 +39,9 @@ describe("/records", () => { const createRecord = async r => await request - .post(`/api/${instance._id}/${model._id}/records`) + .post(`/api/${model._id}/records`) .send(r || record) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -57,14 +57,14 @@ describe("/records", () => { const existing = rec.body const res = await request - .post(`/api/${instance._id}/${model._id}/records`) + .post(`/api/${model._id}/records`) .send({ _id: existing._id, _rev: existing._rev, modelId: model._id, name: "Updated Name", }) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -77,8 +77,8 @@ describe("/records", () => { const existing = rec.body const res = await request - .get(`/api/${instance._id}/${model._id}/records/${existing._id}`) - .set(defaultHeaders) + .get(`/api/${model._id}/records/${existing._id}`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -100,8 +100,8 @@ describe("/records", () => { await createRecord(newRecord) const res = await request - .get(`/api/${instance._id}/${model._id}/records`) - .set(defaultHeaders) + .get(`/api/${model._id}/records`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -122,8 +122,8 @@ describe("/records", () => { const recordIds = [record.body._id, secondRecord.body._id] const res = await request - .post(`/api/${instance._id}/records/search`) - .set(defaultHeaders) + .post(`/api/records/search`) + .set(defaultHeaders(app._id, instance._id)) .send({ keys: recordIds }) @@ -137,8 +137,8 @@ describe("/records", () => { it("load should return 404 when record does not exist", async () => { await createRecord() await request - .get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`) - .set(defaultHeaders) + .get(`/api/${model._id}/records/not-a-valid-id`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(404) }) @@ -147,9 +147,9 @@ describe("/records", () => { describe("validate", () => { it("should return no errors on valid record", async () => { const result = await request - .post(`/api/${instance._id}/${model._id}/records/validate`) + .post(`/api/${model._id}/records/validate`) .send({ name: "ivan" }) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -159,9 +159,9 @@ describe("/records", () => { it("should errors on invalid record", async () => { const result = await request - .post(`/api/${instance._id}/${model._id}/records/validate`) + .post(`/api/${model._id}/records/validate`) .send({ name: 1 }) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index d950f03c3d..a17e6fe275 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -36,11 +36,11 @@ describe("/users", () => { describe("fetch", () => { it("returns a list of users from an instance db", async () => { - await createUser(request, instance._id, "brenda", "brendas_password") - await createUser(request, instance._id, "pam", "pam_password") + await createUser(request, app._id, instance._id, "brenda", "brendas_password") + await createUser(request, app._id, instance._id, "pam", "pam_password") const res = await request - .get(`/api/${instance._id}/users`) - .set(defaultHeaders) + .get(`/api/users`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -50,12 +50,13 @@ describe("/users", () => { }) it("should apply authorization to endpoint", async () => { - await createUser(request, instance._id, "brenda", "brendas_password") + await createUser(request, app._id, instance._id, "brenda", "brendas_password") await testPermissionsForEndpoint({ request, method: "GET", - url: `/api/${instance._id}/users`, + url: `/api/users`, instanceId: instance._id, + appId: app._id, permissionName: LIST_USERS, }) }) @@ -66,8 +67,8 @@ describe("/users", () => { it("returns a success message when a user is successfully created", async () => { const res = await request - .post(`/api/${instance._id}/users`) - .set(defaultHeaders) + .post(`/api/users`) + .set(defaultHeaders(app._id, instance._id)) .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID }) .expect(200) .expect('Content-Type', /json/) @@ -81,8 +82,9 @@ describe("/users", () => { request, method: "POST", body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID }, - url: `/api/${instance._id}/users`, + url: `/api/users`, instanceId: instance._id, + appId: app._id, permissionName: USER_MANAGEMENT, }) }) diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js index b8fd47972a..5a889bed67 100644 --- a/packages/server/src/api/routes/tests/view.spec.js +++ b/packages/server/src/api/routes/tests/view.spec.js @@ -30,7 +30,7 @@ describe("/views", () => { const createView = async () => await request - .post(`/api/${instance._id}/views`) + .post(`/api/views`) .send({ name: "TestView", map: `function(doc) { @@ -40,7 +40,7 @@ describe("/views", () => { }`, reduce: `function(keys, values) { }` }) - .set(defaultHeaders) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -56,14 +56,14 @@ describe("/views", () => { describe("fetch", () => { beforeEach(async () => { - model = await createModel(request, instance._id); + model = await createModel(request, app._id, instance._id); }); it("should only return custom views", async () => { const view = await createView() const res = await request - .get(`/api/${instance._id}/views`) - .set(defaultHeaders) + .get(`/api/views`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) expect(res.body.length).toBe(1) diff --git a/packages/server/src/api/routes/tests/workflow.spec.js b/packages/server/src/api/routes/tests/workflow.spec.js index 1f345698bf..f8b38c53ec 100644 --- a/packages/server/src/api/routes/tests/workflow.spec.js +++ b/packages/server/src/api/routes/tests/workflow.spec.js @@ -63,8 +63,8 @@ describe("/workflows", () => { describe("create", () => { it("returns a success message when the workflow is successfully created", async () => { const res = await request - .post(`/api/${instance._id}/workflows`) - .set(defaultHeaders) + .post(`/api/workflows`) + .set(defaultHeaders(app._id, instance._id)) .send(TEST_WORKFLOW) .expect('Content-Type', /json/) .expect(200) @@ -77,8 +77,9 @@ describe("/workflows", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "POST", - url: `/api/${instance._id}/workflows`, + url: `/api/workflows`, instanceId: instance._id, + appId: app._id, body: TEST_WORKFLOW }) }) @@ -92,8 +93,8 @@ describe("/workflows", () => { workflow.name = "Updated Name"; const res = await request - .put(`/api/${instance._id}/workflows`) - .set(defaultHeaders) + .put(`/api/workflows`) + .set(defaultHeaders(app._id, instance._id)) .send(workflow) .expect('Content-Type', /json/) .expect(200) @@ -107,8 +108,8 @@ describe("/workflows", () => { it("return all the workflows for an instance", async () => { await createWorkflow(); const res = await request - .get(`/api/${instance._id}/workflows`) - .set(defaultHeaders) + .get(`/api/workflows`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -119,8 +120,9 @@ describe("/workflows", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "GET", - url: `/api/${instance._id}/workflows`, + url: `/api/workflows`, instanceId: instance._id, + appId: app._id, }) }) }) @@ -129,8 +131,8 @@ describe("/workflows", () => { it("deletes a workflow by its ID", async () => { await createWorkflow(); const res = await request - .delete(`/api/${instance._id}/workflows/${workflow.id}/${workflow.rev}`) - .set(defaultHeaders) + .delete(`/api/workflows/${workflow.id}/${workflow.rev}`) + .set(defaultHeaders(app._id, instance._id)) .expect('Content-Type', /json/) .expect(200) @@ -142,8 +144,9 @@ describe("/workflows", () => { await builderEndpointShouldBlockNormalUsers({ request, method: "DELETE", - url: `/api/${instance._id}/workflows/${workflow.id}/${workflow._rev}`, + url: `/api/workflows/${workflow.id}/${workflow._rev}`, instanceId: instance._id, + appId: app._id, }) }) }) diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 20e17c7473..3c560d3f80 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -6,19 +6,11 @@ const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch) - .get( - "/api/:instanceId/users/:username", - authorized(USER_MANAGEMENT), - controller.find - ) - .post( - "/api/:instanceId/users", - authorized(USER_MANAGEMENT), - controller.create - ) + .get("/api/users", authorized(LIST_USERS), controller.fetch) + .get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find) + .post("/api/users", authorized(USER_MANAGEMENT), controller.create) .delete( - "/api/:instanceId/users/:username", + "/api/users/:username", authorized(USER_MANAGEMENT), controller.destroy ) diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index 193ece1cdf..5a73eea22e 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -8,13 +8,13 @@ const router = Router() router .get( - "/api/:instanceId/views/:viewName", + "/api/views/:viewName", authorized(READ_VIEW, ctx => ctx.params.viewName), recordController.fetchView ) - .get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch) + .get("/api/views", authorized(BUILDER), viewController.fetch) // .patch("/api/:databaseId/views", controller.update); // .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy); - .post("/api/:instanceId/views", authorized(BUILDER), viewController.create) + .post("/api/views", authorized(BUILDER), viewController.create) module.exports = router diff --git a/packages/server/src/api/routes/workflow.js b/packages/server/src/api/routes/workflow.js index fcb1d6e182..987e18a60f 100644 --- a/packages/server/src/api/routes/workflow.js +++ b/packages/server/src/api/routes/workflow.js @@ -6,20 +6,16 @@ const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/workflows", authorized(BUILDER), controller.fetch) + .get("/api/workflows", authorized(BUILDER), controller.fetch) .get("/api/workflows/:id", authorized(BUILDER), controller.find) .get( - "/api/:instanceId/workflows/:id/:action", + "/api/workflows/:id/:action", authorized(BUILDER), controller.fetchActionScript ) - .put("/api/:instanceId/workflows", authorized(BUILDER), controller.update) - .post("/api/:instanceId/workflows", authorized(BUILDER), controller.create) + .put("/api/workflows", authorized(BUILDER), controller.update) + .post("/api/workflows", authorized(BUILDER), controller.create) .post("/api/workflows/action", controller.executeAction) - .delete( - "/api/:instanceId/workflows/:id/:rev", - authorized(BUILDER), - controller.destroy - ) + .delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy) module.exports = router diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index fdf8b9874a..9bab1ed1a0 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -3,7 +3,6 @@ module.exports = { NODE_ENV: process.env.NODE_ENV, JWT_SECRET: process.env.JWT_SECRET, BUDIBASE_DIR: process.env.BUDIBASE_DIR, - ADMIN_SECRET: process.env.ADMIN_SECRET, PORT: process.env.PORT, COUCH_DB_URL: process.env.COUCH_DB_URL, SALT_ROUNDS: process.env.SALT_ROUNDS, diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index d0ce1e2f30..36e2776abe 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -1,10 +1,11 @@ const jwt = require("jsonwebtoken") const STATUS_CODES = require("../utilities/statusCodes") -const env = require("../environment") const accessLevelController = require("../api/controllers/accesslevel") const { ADMIN_LEVEL_ID, POWERUSER_LEVEL_ID, + BUILDER_LEVEL_ID, + ANON_LEVEL_ID, } = require("../utilities/accessLevels") module.exports = async (ctx, next) => { @@ -15,16 +16,21 @@ module.exports = async (ctx, next) => { const appToken = ctx.cookies.get("budibase:token") const builderToken = ctx.cookies.get("builder:token") - const isBuilderAgent = ctx.headers["x-user-agent"] === "Budibase Builder" - // all admin api access should auth with buildertoken and 'Budibase Builder user agent - const shouldAuthAsBuilder = isBuilderAgent && builderToken - - if (shouldAuthAsBuilder) { - const builderTokenValid = builderToken === env.ADMIN_SECRET - - ctx.isAuthenticated = builderTokenValid - ctx.isBuilder = builderTokenValid + if (builderToken) { + try { + const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret) + ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID + ctx.user = { + ...jwtPayload, + accessLevel: await getAccessLevel( + jwtPayload.instanceId, + jwtPayload.accessLevelId + ), + } + } catch (_) { + // empty: do nothing + } await next() return @@ -46,7 +52,7 @@ module.exports = async (ctx, next) => { jwtPayload.accessLevelId ), } - ctx.isAuthenticated = true + ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID } catch (err) { ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) } @@ -57,7 +63,9 @@ module.exports = async (ctx, next) => { const getAccessLevel = async (instanceId, accessLevelId) => { if ( accessLevelId === POWERUSER_LEVEL_ID || - accessLevelId === ADMIN_LEVEL_ID + accessLevelId === ADMIN_LEVEL_ID || + accessLevelId === BUILDER_LEVEL_ID || + accessLevelId === ANON_LEVEL_ID ) { return { _id: accessLevelId, @@ -69,6 +77,8 @@ const getAccessLevel = async (instanceId, accessLevelId) => { const findAccessContext = { params: { levelId: accessLevelId, + }, + user: { instanceId, }, } diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index b07715af36..b452d63cf5 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -2,6 +2,7 @@ const { adminPermissions, ADMIN_LEVEL_ID, POWERUSER_LEVEL_ID, + BUILDER_LEVEL_ID, BUILDER, } = require("../utilities/accessLevels") @@ -10,7 +11,11 @@ module.exports = (permName, getItemId) => async (ctx, next) => { ctx.throw(403, "Session not authenticated") } - if (ctx.isBuilder) { + if (!ctx.user) { + ctx.throw(403, "User not found") + } + + if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { await next() return } @@ -20,10 +25,6 @@ module.exports = (permName, getItemId) => async (ctx, next) => { return } - if (!ctx.user) { - ctx.throw(403, "User not found") - } - const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) { diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js index 9fff76e531..50ae559d07 100644 --- a/packages/server/src/utilities/accessLevels.js +++ b/packages/server/src/utilities/accessLevels.js @@ -5,6 +5,8 @@ const workflowController = require("../api/controllers/workflow") // Access Level IDs const ADMIN_LEVEL_ID = "ADMIN" const POWERUSER_LEVEL_ID = "POWER_USER" +const BUILDER_LEVEL_ID = "BUILDER" +const ANON_LEVEL_ID = "ANON" // Permissions const READ_MODEL = "read-model" @@ -28,7 +30,7 @@ const generateAdminPermissions = async instanceId => [ const generatePowerUserPermissions = async instanceId => { const fetchModelsCtx = { - params: { + user: { instanceId, }, } @@ -36,7 +38,7 @@ const generatePowerUserPermissions = async instanceId => { const models = fetchModelsCtx.body const fetchViewsCtx = { - params: { + user: { instanceId, }, } @@ -44,7 +46,7 @@ const generatePowerUserPermissions = async instanceId => { const views = fetchViewsCtx.body const fetchWorkflowsCtx = { - params: { + user: { instanceId, }, } @@ -83,6 +85,8 @@ const generatePowerUserPermissions = async instanceId => { module.exports = { ADMIN_LEVEL_ID, POWERUSER_LEVEL_ID, + BUILDER_LEVEL_ID, + ANON_LEVEL_ID, READ_MODEL, WRITE_MODEL, READ_VIEW, @@ -90,6 +94,7 @@ module.exports = { USER_MANAGEMENT, BUILDER, LIST_USERS, + adminPermissions, generateAdminPermissions, generatePowerUserPermissions, } diff --git a/packages/server/src/utilities/builder/buildPage.js b/packages/server/src/utilities/builder/buildPage.js index 3d44cb8072..ea0c9cce25 100644 --- a/packages/server/src/utilities/builder/buildPage.js +++ b/packages/server/src/utilities/builder/buildPage.js @@ -45,18 +45,16 @@ const copyClientLib = async (appPath, pageName) => { const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { const appPublicPath = publicPath(appPath, pageName) - const appRootPath = rootPath(config, appId) const stylesheetUrl = s => s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}` const templateObj = { title: pkg.page.title || "Budibase App", - favicon: `${appRootPath}/${pkg.page.favicon || "/_shared/favicon.png"}`, + favicon: `${pkg.page.favicon || "/_shared/favicon.png"}`, stylesheets: (pkg.page.stylesheets || []).map(stylesheetUrl), screenStyles: pkg.screens.filter(s => s._css).map(s => s._css), pageStyle: pkg.page._css, - appRootPath, } const indexHtmlTemplate = await readFile( @@ -74,7 +72,6 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => { const appPath = appPackageFolder(config, appId) const appPublicPath = publicPath(appPath, pageName) - const appRootPath = rootPath(config, appId) const filename = join(appPublicPath, "clientFrontendDefinition.js") @@ -89,7 +86,6 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => { } const clientUiDefinition = JSON.stringify({ - appRootPath: appRootPath, page: pkg.page, screens: pkg.screens, libraries: [ diff --git a/packages/server/src/utilities/builder/index.template.html b/packages/server/src/utilities/builder/index.template.html index 80df75662b..91b33cb284 100644 --- a/packages/server/src/utilities/builder/index.template.html +++ b/packages/server/src/utilities/builder/index.template.html @@ -24,15 +24,15 @@ {{ /each }} {{ each(options.screenStyles) }} - + {{ /each }} {{ if(options.pageStyle) }} - + {{ /if }} - - + + diff --git a/packages/server/src/utilities/builder/setBuilderToken.js b/packages/server/src/utilities/builder/setBuilderToken.js new file mode 100644 index 0000000000..12622d5522 --- /dev/null +++ b/packages/server/src/utilities/builder/setBuilderToken.js @@ -0,0 +1,19 @@ +const { BUILDER_LEVEL_ID } = require("../accessLevels") +const jwt = require("jsonwebtoken") + +module.exports = (ctx, appId, instanceId) => { + const builderUser = { + userId: "BUILDER", + accessLevelId: BUILDER_LEVEL_ID, + instanceId, + appId, + } + + const token = jwt.sign(builderUser, ctx.config.jwtSecret, { + expiresIn: "30 days", + }) + + var expiry = new Date() + expiry.setDate(expiry.getDate() + 30) + ctx.cookies.set("builder:token", token, { expires: expiry, httpOnly: false }) +} diff --git a/packages/standard-components/public/clientAppDefinition.js b/packages/standard-components/public/clientAppDefinition.js index be67dcf1a8..f77f59ec03 100644 --- a/packages/standard-components/public/clientAppDefinition.js +++ b/packages/standard-components/public/clientAppDefinition.js @@ -98,6 +98,5 @@ window["##BUDIBASE_APPDEFINITION##"] = { nodeId: 0, }, componentLibraries: ["budibase-standard-components"], - appRootPath: "/testApp2", props: {}, } diff --git a/packages/standard-components/src/DataChart.svelte b/packages/standard-components/src/DataChart.svelte index 3f7aafd8a8..bccfc6314a 100644 --- a/packages/standard-components/src/DataChart.svelte +++ b/packages/standard-components/src/DataChart.svelte @@ -8,7 +8,6 @@ fcRoot(FusionCharts, Charts, FusionTheme) export let _bb - export let _instanceId export let model export let type = "column2d" @@ -25,7 +24,7 @@ } async function fetchData() { - const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}` + const FETCH_RECORDS_URL = `/api/views/all_${model}` const response = await _bb.api.get(FETCH_RECORDS_URL) if (response.status === 200) { const json = await response.json() diff --git a/packages/standard-components/src/DataForm.svelte b/packages/standard-components/src/DataForm.svelte index f199a2bd62..1510110ba9 100644 --- a/packages/standard-components/src/DataForm.svelte +++ b/packages/standard-components/src/DataForm.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" export let _bb - export let _instanceId export let model let username @@ -21,19 +20,19 @@ $: fields = Object.keys(schema) async function fetchModel() { - const FETCH_MODEL_URL = `/api/${_instanceId}/models/${model}` + const FETCH_MODEL_URL = `/api/models/${model}` const response = await _bb.api.get(FETCH_MODEL_URL) modelDef = await response.json() schema = modelDef.schema } async function save() { - const SAVE_RECORD_URL = `/api/${_instanceId}/${model}/records` + const SAVE_RECORD_URL = `/api/${model}/records` const response = await _bb.api.post(SAVE_RECORD_URL, newModel) const json = await response.json() store.update(state => { - state[model._id] = [...state[model], json] + state[model] = state[model] ? [...state[model], json] : [json] return state }) } diff --git a/packages/standard-components/src/DataList.svelte b/packages/standard-components/src/DataList.svelte index 1e71f706d5..9ef0b92d2c 100644 --- a/packages/standard-components/src/DataList.svelte +++ b/packages/standard-components/src/DataList.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" export let _bb - export let _instanceId export let model export let layout = "list" @@ -12,7 +11,7 @@ async function fetchData() { if (!model || !model.length) return - const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}` + const FETCH_RECORDS_URL = `/api/views/all_${model}` const response = await _bb.api.get(FETCH_RECORDS_URL) if (response.status === 200) { const json = await response.json() diff --git a/packages/standard-components/src/DataTable.svelte b/packages/standard-components/src/DataTable.svelte index e1e32cb9f9..e6f097314c 100644 --- a/packages/standard-components/src/DataTable.svelte +++ b/packages/standard-components/src/DataTable.svelte @@ -3,14 +3,13 @@ export let _bb export let onLoad - export let _instanceId export let model let headers = [] let store = _bb.store async function fetchData() { - const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}` + const FETCH_RECORDS_URL = `/api/views/all_${model}` const response = await _bb.api.get(FETCH_RECORDS_URL) if (response.status === 200) { diff --git a/packages/standard-components/src/List.svelte b/packages/standard-components/src/List.svelte index b335b71a78..8207ef47ee 100644 --- a/packages/standard-components/src/List.svelte +++ b/packages/standard-components/src/List.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" export let _bb - export let _instanceId export let model export let layout = "list" @@ -13,7 +12,7 @@ async function fetchData() { if (!model || !model.length) return - const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}` + const FETCH_RECORDS_URL = `/api/views/all_${model}` const response = await _bb.api.get(FETCH_RECORDS_URL) if (response.status === 200) { const json = await response.json() diff --git a/packages/standard-components/src/Login.svelte b/packages/standard-components/src/Login.svelte index 165da75e24..79ea03d990 100644 --- a/packages/standard-components/src/Login.svelte +++ b/packages/standard-components/src/Login.svelte @@ -13,30 +13,17 @@ let password = "" let loading = false let error = false - let _logo = "" let _buttonClass = "" let _inputClass = "" $: { - _logo = _bb.relativeUrl(logo) _buttonClass = buttonClass || "default-button" _inputClass = inputClass || "default-input" } const login = async () => { loading = true - const response = await fetch(_bb.relativeUrl("/api/authenticate"), { - body: JSON.stringify({ - username, - password, - }), - headers: { - "Content-Type": "application/json", - "x-user-agent": "Budibase Builder", - }, - method: "POST", - }) - + const response = await _bb.api.post("/api/authenticate", { username, password }) if (response.status === 200) { const json = await response.json() localStorage.setItem("budibase:token", json.token) @@ -48,33 +35,13 @@ } } -
    -
    -
    - {#if _logo} -
    - logo -
    - {/if} -

    Log in to {name}

    -
    -
    - -
    - -
    - -
    +
    +
    + {#if logo} +
    + logo