diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index 53e1b90d73..baa8fc40dc 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -147,7 +147,7 @@ exports.getRole = async (appId, roleId) => { */ async function getAllUserRoles(appId, userRoleId) { if (!userRoleId) { - return [BUILTIN_IDS.PUBLIC] + return [BUILTIN_IDS.BASIC] } let currentRole = await exports.getRole(appId, userRoleId) let roles = currentRole ? [currentRole] : [] @@ -226,7 +226,7 @@ exports.getAllRoles = async appId => { dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId )[0] if (dbBuiltin == null) { - roles.push(builtinRole) + roles.push(builtinRole || builtinRoles.BASIC) } else { // remove role and all back after combining with the builtin roles = roles.filter(role => role._id !== dbBuiltin._id) diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte index 3e1080f2cd..77a8526a15 100644 --- a/packages/bbui/src/Tabs/Tabs.svelte +++ b/packages/bbui/src/Tabs/Tabs.svelte @@ -15,8 +15,12 @@ const dispatch = createEventDispatcher() - $: selected = $tab.title - $: selected = dispatch("select", selected) + $: { + if ($tab.title !== selected) { + selected = $tab.title + dispatch("select", selected) + } + } let top, left, width, height $: calculateIndicatorLength($tab) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 9746b43103..ac837978a9 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -137,6 +137,9 @@ export const getFrontendStore = () => { save: async screen => { const creatingNewScreen = screen._id === undefined const response = await api.post(`/api/screens`, screen) + if (response.status !== 200) { + return + } screen = await response.json() await store.actions.routing.fetch() diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte index f58b9f197f..8b7417c41f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -10,8 +10,10 @@ let selectedRole = {} let errors = [] let builtInRoles = ["Admin", "Power", "Basic", "Public"] + // Don't allow editing of public role + $: editableRoles = $roles.filter(role => role._id !== "PUBLIC") $: selectedRoleId = selectedRole._id - $: otherRoles = $roles.filter(role => role._id !== selectedRoleId) + $: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId) $: isCreating = selectedRoleId == null || selectedRoleId === "" const fetchBasePermissions = async () => { @@ -96,7 +98,7 @@ label="Role" value={selectedRoleId} on:change={changeRole} - options={$roles} + options={editableRoles} placeholder="Create new role" getOptionValue={role => role._id} getOptionLabel={role => role.name} diff --git a/packages/builder/src/components/design/AppPreview/iframeTemplate.js b/packages/builder/src/components/design/AppPreview/iframeTemplate.js index eb4f22f857..b659a041e4 100644 --- a/packages/builder/src/components/design/AppPreview/iframeTemplate.js +++ b/packages/builder/src/components/design/AppPreview/iframeTemplate.js @@ -27,8 +27,7 @@ export default ` align-items: stretch; } html.loaded { - box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.1); - + box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.1); } body { flex: 1 1 auto; diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte index 2a86afbf9a..1d2d772a50 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte @@ -21,7 +21,7 @@ export let value = [] export let componentInstance let drawer - let tempValue = value + let tempValue = value || [] $: numFilters = Array.isArray(tempValue) ? tempValue.length @@ -31,15 +31,6 @@ $: schemaFields = Object.values(schema || {}) $: internalTable = dataSource?.type === "table" - // Reset value if value is wrong type for the datasource. - // Lucene editor needs an array, and simple editor needs an object. - $: { - if (!Array.isArray(value)) { - tempValue = [] - dispatch("change", []) - } - } - const saveFilter = async () => { dispatch("change", tempValue) notifications.success("Filters saved.") diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte index 0296f2e5b7..69b6e770f6 100644 --- a/packages/builder/src/pages/builder/apps/index.svelte +++ b/packages/builder/src/pages/builder/apps/index.svelte @@ -28,12 +28,7 @@ onMount(async () => { await organisation.init() await apps.load() - // Skip the portal if you only have one app - if (!$auth.isBuilder && $apps.filter(publishedAppsOnly).length === 1) { - window.location = `/${publishedApps[0].prodId}` - } else { - loaded = true - } + loaded = true }) const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED diff --git a/packages/builder/src/pages/builder/portal/manage/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/_layout.svelte index 98ae140b25..e6c73bc596 100644 --- a/packages/builder/src/pages/builder/portal/manage/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/manage/_layout.svelte @@ -9,8 +9,6 @@ $redirect("../") } } - - $: console.log($page) {#if $auth.isAdmin} diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index 912506d0cf..8e029d73b8 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -33,7 +33,7 @@ role: {}, } - $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "" + $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "BASIC" // Merge the Apps list and the roles response to get something that makes sense for the table $: appList = Object.keys($apps?.data).map(id => { const role = $userFetch?.data?.roles?.[id] || defaultRoleId diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte index e881fa37d2..332be8e2d4 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte @@ -9,7 +9,9 @@ const dispatch = createEventDispatcher() const roles = app.roles - let options = roles.map(role => ({ value: role._id, label: role.name })) + let options = roles + .map(role => ({ value: role._id, label: role.name })) + .filter(role => role.value !== "PUBLIC") let selectedRole = user?.roles?.[app?._id] async function updateUserRoles() { diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index fcdd9dbd5b..280b580164 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -43,10 +43,9 @@ const makeApiCall = async ({ method, url, body, json = true }) => { case 400: return handleError(`${url}: Bad Request`) case 403: - // reload the page incase the token has expired - if (!url.includes("self")) { - location.reload() - } + notificationStore.danger( + "Your session has expired, or you don't have permission to access that data" + ) return handleError(`${url}: Forbidden`) default: if (response.status >= 200 && response.status < 400) { diff --git a/packages/client/src/api/auth.js b/packages/client/src/api/auth.js index 426d4f08d0..6ea105d9f9 100644 --- a/packages/client/src/api/auth.js +++ b/packages/client/src/api/auth.js @@ -24,7 +24,12 @@ export const logIn = async ({ email, password }) => { export const fetchSelf = async () => { const user = await API.get({ url: "/api/self" }) if (user?._id) { - return (await enrichRows([user], TableNames.USERS))[0] + if (user.roleId === "PUBLIC") { + // Don't try to enrich a public user as it will 403 + return user + } else { + return (await enrichRows([user], TableNames.USERS))[0] + } } else { return null } diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 2675531a63..1076c0f568 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -19,6 +19,8 @@ import SettingsBar from "./preview/SettingsBar.svelte" import SelectionIndicator from "./preview/SelectionIndicator.svelte" import HoverIndicator from "./preview/HoverIndicator.svelte" + import { Layout, Heading, Body } from "@budibase/bbui" + import ErrorSVG from "../../../builder/assets/error.svg" // Provide contexts setContext("sdk", SDK) @@ -26,6 +28,7 @@ setContext("context", createContextStore()) let dataLoaded = false + let permissionError = false // Load app config onMount(async () => { @@ -47,12 +50,21 @@ }, ] - // Redirect to home layout if no matching route + // Handle no matching route - this is likely a permission error $: { if (dataLoaded && $routeStore.routerLoaded && !$routeStore.activeRoute) { if ($authStore) { - routeStore.actions.navigate("/") + // There is a logged in user, so handle them + if ($screenStore.screens.length) { + // Screens exist so navigate back to the home screen + const firstRoute = $screenStore.screens[0].routing?.route ?? "/" + routeStore.actions.navigate(firstRoute) + } else { + // No screens likely means the user has no permissions to view this app + permissionError = true + } } else { + // The user is not logged in, redirect them to login const returnUrl = `${window.location.pathname}${window.location.hash}` const encodedUrl = encodeURIComponent(returnUrl) window.location = `/builder/auth/login?returnUrl=${encodedUrl}` @@ -64,36 +76,46 @@ $builderStore.theme || $appStore.application?.theme || "spectrum--light" -{#if dataLoaded && $screenStore.activeLayout} +{#if dataLoaded}
- -
- {#key $screenStore.activeLayout._id} - - {/key} + {#if permissionError} +
+ + {@html ErrorSVG} + You don't have permission to use this app + Ask your administrator to grant you access +
- - - - {#key $builderStore.selectedComponentId} + {:else if $screenStore.activeLayout} + +
+ {#key $screenStore.activeLayout._id} + + {/key} +
+ + + + {#key $builderStore.selectedComponentId} + {#if $builderStore.inBuilder} + + {/if} + {/key} + {#if $builderStore.inBuilder} - + + {/if} - {/key} - - {#if $builderStore.inBuilder} - - - {/if} -
+ + {/if}
{/if} @@ -131,4 +153,31 @@ scrollbar-color: var(--spectrum-global-color-gray-400) var(--spectrum-alias-background-color-default); } + + .error { + position: absolute; + width: 100%; + height: 100%; + display: grid; + place-items: center; + z-index: 1; + text-align: center; + padding: 20px; + } + .error :global(svg) { + fill: var(--spectrum-global-color-gray-500); + width: 80px; + height: 80px; + } + .error :global(h1), + .error :global(p) { + color: var(--spectrum-global-color-gray-800); + } + .error :global(p) { + font-style: italic; + margin-top: -0.5em; + } + .error :global(h1) { + font-weight: 400; + } diff --git a/packages/client/src/store/screens.js b/packages/client/src/store/screens.js index c771f6e277..367d9ecfea 100644 --- a/packages/client/src/store/screens.js +++ b/packages/client/src/store/screens.js @@ -7,17 +7,20 @@ const createScreenStore = () => { const store = derived( [appStore, routeStore, builderStore], ([$appStore, $routeStore, $builderStore]) => { - let activeLayout - let activeScreen + let activeLayout, activeScreen + let layouts, screens if ($builderStore.inBuilder) { // Use builder defined definitions if inside the builder preview activeLayout = $builderStore.layout activeScreen = $builderStore.screen + layouts = [activeLayout] + screens = [activeScreen] } else { activeLayout = { props: { _component: "screenslot" } } // Find the correct screen by matching the current route - const { screens, layouts } = $appStore + screens = $appStore.screens + layouts = $appStore.layouts if ($routeStore.activeRoute) { activeScreen = screens.find( screen => screen._id === $routeStore.activeRoute.screenId @@ -29,7 +32,7 @@ const createScreenStore = () => { ) } } - return { activeLayout, activeScreen } + return { layouts, screens, activeLayout, activeScreen } } ) diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte index 670a0f22e2..4943051328 100644 --- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte +++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte @@ -34,13 +34,55 @@ *:after { box-sizing: border-box; } + + #error { + position: absolute; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + display: none; + font-family: "Source Sans Pro", sans-serif; + flex-direction: column; + justify-content: center; + align-items: center; + background: #222; + text-align: center; + padding: 2rem; + gap: 2rem; + } + #error h1, + #error h2 { + margin: 0; + } + #error h1 { + color: #ccc; + font-size: 3rem; + font-weight: 600; + } + #error h2 { + color: #888; + font-weight: 400; + } +
+

There was an error loading your app

+

+ The Budibase client library could not be loaded. Try republishing your + app. +

+
diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 1930d0a4ec..2333787e6e 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -9,9 +9,13 @@ jest.mock("../../../utilities/redis", () => ({ updateLock: jest.fn(), setDebounce: jest.fn(), checkDebounce: jest.fn(), + shutdown: jest.fn(), })) -const { clearAllApps, checkBuilderEndpoint } = require("./utilities/TestFunctions") +const { + clearAllApps, + checkBuilderEndpoint, +} = require("./utilities/TestFunctions") const setup = require("./utilities") const { AppStatus } = require("../../../db/utils") @@ -32,7 +36,7 @@ describe("/applications", () => { .post("/api/applications") .send({ name: "My App" }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body._id).toBeDefined() }) @@ -42,7 +46,7 @@ describe("/applications", () => { config, method: "POST", url: `/api/applications`, - body: { name: "My App" } + body: { name: "My App" }, }) }) }) @@ -55,7 +59,7 @@ describe("/applications", () => { const res = await request .get(`/api/applications?status=${AppStatus.DEV}`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) // two created apps + the inited app @@ -68,7 +72,7 @@ describe("/applications", () => { const res = await request .get(`/api/applications/${config.getAppId()}/definition`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) // should have empty packages expect(res.body.screens.length).toEqual(1) @@ -81,7 +85,7 @@ describe("/applications", () => { const res = await request .get(`/api/applications/${config.getAppId()}/appPackage`) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.application).toBeDefined() expect(res.body.screens.length).toEqual(1) @@ -94,10 +98,10 @@ describe("/applications", () => { const res = await request .put(`/api/applications/${config.getAppId()}`) .send({ - name: "TEST_APP" + name: "TEST_APP", }) .set(config.defaultHeaders()) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.rev).toBeDefined() }) @@ -113,14 +117,14 @@ describe("/applications", () => { name: "UPDATED_NAME", }) .set(headers) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(res.body.rev).toBeDefined() // retrieve the app to check it const getRes = await request .get(`/api/applications/${config.getAppId()}/appPackage`) .set(headers) - .expect('Content-Type', /json/) + .expect("Content-Type", /json/) .expect(200) expect(getRes.body.application.updatedAt).toBeDefined() }) diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index 683b7f8ef3..0e9591456c 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -45,10 +45,10 @@ module.exports = async (ctx, next) => { updateCookie = true appId = requestAppId // retrieving global user gets the right role - roleId = globalUser.roleId || BUILTIN_ROLE_IDS.PUBLIC + roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC } else if (appCookie != null) { appId = appCookie.appId - roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC + roleId = appCookie.roleId || BUILTIN_ROLE_IDS.BASIC } // nothing more to do if (!appId) { diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index ddda274ef5..afacbf8cdf 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -238,7 +238,10 @@ exports.readFileSync = (filepath, options = "utf8") => { */ exports.cleanup = appIds => { for (let appId of appIds) { - fs.rmdirSync(join(budibaseTempDir(), appId), { recursive: true }) + const path = join(budibaseTempDir(), appId) + if (fs.existsSync(path)) { + fs.rmdirSync(path, { recursive: true }) + } } } diff --git a/packages/server/src/utilities/global.js b/packages/server/src/utilities/global.js index 17ce066551..eddbd63cd7 100644 --- a/packages/server/src/utilities/global.js +++ b/packages/server/src/utilities/global.js @@ -19,7 +19,7 @@ exports.updateAppRole = (appId, user) => { if (!user.roleId && user.builder && user.builder.global) { user.roleId = BUILTIN_ROLE_IDS.ADMIN } else if (!user.roleId) { - user.roleId = BUILTIN_ROLE_IDS.PUBLIC + user.roleId = BUILTIN_ROLE_IDS.BASIC } delete user.roles return user diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index c49817fbd0..be66a4ce87 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -73,13 +73,13 @@ { "label": "Column", "value": "column", - "barIcon": "ViewRow", + "barIcon": "ViewColumn", "barTitle": "Column layout" }, { "label": "Row", "value": "row", - "barIcon": "ViewColumn", + "barIcon": "ViewRow", "barTitle": "Row layout" } ],