1
0
Fork 0
mirror of synced 2024-10-04 03:54:37 +13:00

Merge pull request #10363 from Budibase/fix/budi-6900

Removing "export all apps" functionality from cloud
This commit is contained in:
Michael Drury 2023-04-21 12:29:07 +01:00 committed by GitHub
commit 76625cd509
13 changed files with 14 additions and 405 deletions

View file

@ -1,46 +0,0 @@
<script>
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
import { API } from "api"
import { admin } from "stores/portal"
let submitting = false
$: value = { file: null }
async function importApps() {
submitting = true
try {
// Create form data to create app
let data = new FormData()
data.append("importFile", value.file)
// Create App
await API.importApps(data)
await admin.checkImportComplete()
notifications.success("Import complete, please finish registration!")
} catch (error) {
notifications.error("Failed to import apps")
}
submitting = false
}
</script>
<ModalContent
title="Import apps"
confirmText="Import apps"
onConfirm={importApps}
disabled={!value.file}
>
<Body>
Please upload the file that was exported from your Cloud environment to get
started
</Body>
<Dropzone
gallery={false}
label="File to import"
value={[value.file]}
on:change={e => {
value.file = e.detail?.[0]
}}
/>
</ModalContent>

View file

@ -1,31 +1,19 @@
<script>
import {
Button,
Heading,
notifications,
Layout,
Body,
Modal,
} from "@budibase/bbui"
import { Button, Heading, notifications, Layout, Body } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { API } from "api"
import { admin, auth } from "stores/portal"
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
import { FancyForm, FancyInput, ActionButton } from "@budibase/bbui"
import { FancyForm, FancyInput } from "@budibase/bbui"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { passwordsMatch, handleError } from "../auth/_components/utils"
let modal
let form
let errors = {}
let formData = {}
let submitted = false
$: tenantId = $auth.tenantId
$: cloud = $admin.cloud
$: imported = $admin.importComplete
async function save() {
form.validate()
@ -46,22 +34,8 @@
notifications.error("Failed to create admin user")
}
}
onMount(async () => {
if (!cloud) {
try {
await admin.checkImportComplete()
} catch (error) {
notifications.error("Error checking import status")
}
}
})
</script>
<Modal bind:this={modal} padding={false} width="600px">
<ImportAppsModal />
</Modal>
<TestimonialPage>
<Layout gap="M" noPadding>
<Layout justifyItems="center" noPadding>
@ -156,20 +130,6 @@
Create super admin user
</Button>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<div class="user-actions">
{#if !cloud && !imported}
<ActionButton
quiet
on:click={() => {
modal.show()
}}
>
Import from cloud
</ActionButton>
{/if}
</div>
</Layout>
</Layout>
</TestimonialPage>

View file

@ -9,14 +9,11 @@
notifications,
Notification,
Body,
Icon,
Search,
InlineAlert,
} from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { store, automationStore } from "builderStore"
import { API } from "api"
@ -33,11 +30,9 @@
let appLimitModal
let creatingApp = false
let searchTerm = ""
let cloud = $admin.cloud
let creatingFromTemplate = false
let automationErrors
let accessFilterList = null
let confirmDownloadDialog
$: welcomeHeader = `Welcome ${$auth?.user?.firstName || "back"}`
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
@ -123,15 +118,6 @@
}
}
const initiateAppsExport = () => {
try {
window.location = `/api/cloud/export`
notifications.success("Apps exported successfully")
} catch (err) {
notifications.error(`Error exporting apps: ${err}`)
}
}
const initiateAppImport = () => {
template = { fromFile: true }
creationModal.show()
@ -264,13 +250,6 @@
</div>
{#if enrichedApps.length > 1}
<div class="app-actions">
{#if cloud}
<Icon
name="Download"
hoverable
on:click={confirmDownloadDialog.show}
/>
{/if}
<Select
autoWidth
bind:value={sortBy}
@ -316,18 +295,6 @@
<AppLimitModal bind:this={appLimitModal} />
<ConfirmDialog
bind:this={confirmDownloadDialog}
okText="Continue"
onOk={initiateAppsExport}
warning={false}
title="Download all apps"
>
<InlineAlert
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
/>
</ConfirmDialog>
<style>
.title {
display: flex;

View file

@ -37,14 +37,6 @@ export function createAdminStore() {
})
}
async function checkImportComplete() {
const result = await API.checkImportComplete()
admin.update(store => {
store.importComplete = result ? result.imported : false
return store
})
}
async function getEnvironment() {
const environment = await API.getEnvironment()
admin.update(store => {
@ -92,7 +84,6 @@ export function createAdminStore() {
return {
subscribe: admin.subscribe,
init,
checkImportComplete,
unload,
getChecklist,
}

View file

@ -24,7 +24,6 @@ vi.mock("svelte/store", () => {
vi.mock("api", () => {
return {
API: {
checkImportComplete: vi.fn(),
getEnvironment: vi.fn(),
getSystemStatus: vi.fn(),
getChecklist: vi.fn(),
@ -55,7 +54,6 @@ describe("admin store", () => {
expect(ctx.returnedStore).toEqual({
subscribe: expect.toBe(ctx.writableReturn.subscribe),
init: expect.toBeFunc(),
checkImportComplete: expect.toBeFunc(),
unload: expect.toBeFunc(),
getChecklist: expect.toBeFunc(),
})
@ -205,37 +203,6 @@ describe("admin store", () => {
})
})
describe("checkImportComplete", () => {
describe("import complete", () => {
beforeEach(async ctx => {
API.checkImportComplete.mockReturnValue({ imported: true })
await ctx.returnedStore.checkImportComplete()
})
it("updates the store's importComplete parameter", ctx => {
expect(ctx.writableReturn.update.calls[0][0]({ foo: "foo" })).toEqual({
foo: "foo",
importComplete: true,
})
})
})
describe("import not complete", () => {
beforeEach(async ctx => {
// Can be null
API.checkImportComplete.mockReturnValue(null)
await ctx.returnedStore.checkImportComplete()
})
it("updates the store's importComplete parameter", ctx => {
expect(ctx.writableReturn.update.calls[0][0]({ foo: "foo" })).toEqual({
foo: "foo",
importComplete: false,
})
})
})
})
describe("unload", () => {
beforeEach(ctx => {
ctx.returnedStore.unload()

View file

@ -1,13 +1,4 @@
export const buildOtherEndpoints = API => ({
/**
* TODO: find out what this is
*/
checkImportComplete: async () => {
return await API.get({
url: "/api/cloud/import/complete",
})
},
/**
* Gets the current environment details.
*/

View file

@ -1,119 +0,0 @@
import env from "../../environment"
import { db as dbCore, tenancy } from "@budibase/backend-core"
import { streamFile } from "../../utilities/fileSystem"
import { stringToReadStream } from "../../utilities"
import { getDocParams, DocumentType, isDevAppID } from "../../db/utils"
import { create } from "./application"
import { join } from "path"
import sdk from "../../sdk"
import { App, Ctx, Database } from "@budibase/types"
async function createApp(appName: string, appDirectory: string) {
const ctx = {
request: {
body: {
useTemplate: true,
name: appName,
},
files: {
templateFile: {
path: appDirectory,
},
},
},
}
// @ts-ignore
return create(ctx)
}
async function getAllDocType(db: Database, docType: string) {
const response = await db.allDocs(
getDocParams(docType, null, {
include_docs: true,
})
)
return response.rows.map(row => row.doc)
}
export async function exportApps(ctx: Ctx) {
if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
}
const apps = (await dbCore.getAllApps({ all: true })) as App[]
const globalDBString = await sdk.backups.exportDB(dbCore.getGlobalDBName(), {
filter: (doc: any) => !doc._id.startsWith(DocumentType.USER),
})
// only export the dev apps as they will be the latest, the user can republish the apps
// in their self-hosted environment
let appMetadata = apps
.filter((app: App) => isDevAppID(app.appId || app._id))
.map((app: App) => ({ appId: (app.appId || app._id)!, name: app.name }))
const tmpPath = await sdk.backups.exportMultipleApps(
appMetadata,
globalDBString
)
const filename = `cloud-export-${new Date().getTime()}.tar.gz`
ctx.attachment(filename)
ctx.body = streamFile(tmpPath)
}
async function checkHasBeenImported() {
if (!env.SELF_HOSTED) {
return true
}
const apps = await dbCore.getAllApps({ all: true })
return apps.length !== 0
}
export async function hasBeenImported(ctx: Ctx) {
ctx.body = {
imported: await checkHasBeenImported(),
}
}
export async function importApps(ctx: Ctx) {
if (!env.SELF_HOSTED) {
ctx.throw(400, "Importing only allowed in self hosted environments.")
}
const beenImported = await checkHasBeenImported()
if (beenImported || !ctx.request.files || !ctx.request.files.importFile) {
ctx.throw(
400,
"Import file is required and environment must be fresh to import apps."
)
}
const file = ctx.request.files.importFile as any
if (Array.isArray(file)) {
ctx.throw(400, "Single file is required")
}
if (file.type !== "application/gzip" && file.type !== "application/x-gzip") {
ctx.throw(400, "Import file must be a gzipped tarball.")
}
// initially get all the app databases out of the tarball
const tmpPath = sdk.backups.untarFile(file)
const globalDbImport = sdk.backups.getGlobalDBFile(tmpPath)
const appNames = sdk.backups.getListOfAppsInMulti(tmpPath)
const globalDb = tenancy.getGlobalDB()
// load the global db first
await globalDb.load(stringToReadStream(globalDbImport))
for (let appName of appNames) {
await createApp(appName, join(tmpPath, appName))
}
// if there are any users make sure to remove them
let users = await getAllDocType(globalDb, DocumentType.USER)
let userDeletionPromises = []
for (let user of users) {
userDeletionPromises.push(globalDb.remove(user._id, user._rev))
}
if (userDeletionPromises.length > 0) {
await Promise.all(userDeletionPromises)
}
await globalDb.bulkDocs(users)
ctx.body = {
message: "Apps successfully imported.",
}
}

View file

@ -4,7 +4,6 @@ import currentApp from "../middleware/currentapp"
import zlib from "zlib"
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
import pkg from "../../package.json"
import env from "../environment"
import { middleware as pro } from "@budibase/pro"
export { shutdown } from "./routes/public"
const compress = require("koa-compress")

View file

@ -1,18 +0,0 @@
import Router from "@koa/router"
import * as controller from "../controllers/cloud"
import authorized from "../../middleware/authorized"
import { permissions } from "@budibase/backend-core"
const router: Router = new Router()
router
.get(
"/api/cloud/export",
authorized(permissions.BUILDER),
controller.exportApps
)
// has to be public, only run if apps don't exist
.post("/api/cloud/import", controller.importApps)
.get("/api/cloud/import/complete", controller.hasBeenImported)
export default router

View file

@ -22,7 +22,6 @@ import queryRoutes from "./query"
import backupRoutes from "./backup"
import metadataRoutes from "./metadata"
import devRoutes from "./dev"
import cloudRoutes from "./cloud"
import migrationRoutes from "./migrations"
import pluginRoutes from "./plugin"
import opsRoutes from "./ops"
@ -60,7 +59,6 @@ export const mainRoutes: Router[] = [
queryRoutes,
metadataRoutes,
devRoutes,
cloudRoutes,
rowRoutes,
migrationRoutes,
pluginRoutes,

View file

@ -1,54 +0,0 @@
import { App } from "@budibase/types"
jest.setTimeout(30000)
import { AppStatus } from "../../../db/utils"
import * as setup from "./utilities"
import { wipeDb } from "./utilities/TestFunctions"
import { tenancy } from "@budibase/backend-core"
describe("/cloud", () => {
let request = setup.getRequest()!
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
// Importing is only allowed in self hosted environments
await config.init()
config.modeSelf()
})
describe("import", () => {
it("should be able to import apps", async () => {
// first we need to delete any existing apps on the system so it looks clean otherwise the
// import will not run
await wipeDb()
// Perform the import
const res = await request
.post(`/api/cloud/import`)
.set(config.publicHeaders())
.attach("importFile", "src/api/routes/tests/data/export-test.tar.gz")
.expect(200)
expect(res.body.message).toEqual("Apps successfully imported.")
// get a count of apps after the import
const postImportApps = await request
.get(`/api/applications?status=${AppStatus.ALL}`)
.set(config.publicHeaders())
.expect("Content-Type", /json/)
.expect(200)
const apps = postImportApps.body as App[]
// There are two apps in the file that was imported so check for this
expect(apps.length).toEqual(2)
// The new tenant id was assigned to the imported apps
expect(tenancy.getTenantIDFromAppID(apps[0].appId)).toBe(
config.getTenantId()
)
})
})
})

View file

@ -148,41 +148,6 @@ export async function exportApp(appId: string, config?: ExportOpts) {
}
}
/**
* Export all apps + global DB (if supplied) to a single tarball, this includes
* the attachments for each app as well.
* @param {object[]} appMetadata The IDs and names of apps to export.
* @param {string} globalDbContents The contents of the global DB to export as well.
* @return {string} The path to the tarball.
*/
export async function exportMultipleApps(
appMetadata: { appId: string; name: string }[],
globalDbContents?: string
) {
const tmpPath = join(budibaseTempDir(), uuid())
fs.mkdirSync(tmpPath)
let exportPromises: Promise<void>[] = []
// export each app to a directory, then move it into the complete export
const exportAndMove = async (appId: string, appName: string) => {
const path = await exportApp(appId)
await fs.promises.rename(path, join(tmpPath, appName))
}
for (let metadata of appMetadata) {
exportPromises.push(exportAndMove(metadata.appId, metadata.name))
}
// wait for all exports to finish
await Promise.all(exportPromises)
// add the global DB contents
if (globalDbContents) {
fs.writeFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), globalDbContents)
}
const appNames = appMetadata.map(metadata => metadata.name)
const tarPath = tarFilesToTmp(tmpPath, [...appNames, GLOBAL_DB_EXPORT_FILE])
// clear up the tmp path now tarball generated
fs.rmSync(tmpPath, { recursive: true, force: true })
return tarPath
}
/**
* Streams a backup of the database state for an app
* @param {string} appId The ID of the app which is to be backed up.

View file

@ -18104,9 +18104,9 @@ number-is-nan@^1.0.0:
integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
nunjucks@^3.2.3:
version "3.2.4"
resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.4.tgz#f0878eef528ce7b0aa35d67cc6898635fd74649e"
integrity sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==
version "3.2.3"
resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.3.tgz#1b33615247290e94e28263b5d855ece765648a31"
integrity sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==
dependencies:
a-sync-waterfall "^1.0.0"
asap "^2.0.3"
@ -24445,7 +24445,7 @@ vlq@^0.2.2:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
vm2@3.9.17, vm2@^3.9.11, vm2@^3.9.15, vm2@^3.9.4:
vm2@3.9.17:
version "3.9.17"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.17.tgz#251b165ff8a0e034942b5181057305e39570aeab"
integrity sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==
@ -24453,6 +24453,14 @@ vm2@3.9.17, vm2@^3.9.11, vm2@^3.9.15, vm2@^3.9.4:
acorn "^8.7.0"
acorn-walk "^8.2.0"
vm2@^3.9.11, vm2@^3.9.15, vm2@^3.9.4:
version "3.9.16"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.16.tgz#0fbc2a265f7bf8b837cea6f4a908f88a3f93b8e6"
integrity sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==
dependencies:
acorn "^8.7.0"
acorn-walk "^8.2.0"
vuvuzela@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b"