@budibase/server 2.7.24 → 2.7.25-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,7 +20,7 @@ import Sql from "./base/sql"
20
20
  import { PostgresColumn } from "./base/types"
21
21
  import { escapeDangerousCharacters } from "../utilities"
22
22
 
23
- import { Client, types } from "pg"
23
+ import { Client, ClientConfig, types } from "pg"
24
24
 
25
25
  // Return "date" and "timestamp" types as plain strings.
26
26
  // This lets us reference the original stored timezone.
@@ -42,6 +42,8 @@ interface PostgresConfig {
42
42
  schema: string
43
43
  ssl?: boolean
44
44
  ca?: string
45
+ clientKey?: string
46
+ clientCert?: string
45
47
  rejectUnauthorized?: boolean
46
48
  }
47
49
 
@@ -98,6 +100,19 @@ const SCHEMA: Integration = {
98
100
  required: false,
99
101
  },
100
102
  ca: {
103
+ display: "Server CA",
104
+ type: DatasourceFieldType.LONGFORM,
105
+ default: false,
106
+ required: false,
107
+ },
108
+ clientKey: {
109
+ display: "Client key",
110
+ type: DatasourceFieldType.LONGFORM,
111
+ default: false,
112
+ required: false,
113
+ },
114
+ clientCert: {
115
+ display: "Client cert",
101
116
  type: DatasourceFieldType.LONGFORM,
102
117
  default: false,
103
118
  required: false,
@@ -144,12 +159,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
144
159
  super(SqlClient.POSTGRES)
145
160
  this.config = config
146
161
 
147
- let newConfig = {
162
+ let newConfig: ClientConfig = {
148
163
  ...this.config,
149
164
  ssl: this.config.ssl
150
165
  ? {
151
166
  rejectUnauthorized: this.config.rejectUnauthorized,
152
167
  ca: this.config.ca,
168
+ key: this.config.clientKey,
169
+ cert: this.config.clientCert,
153
170
  }
154
171
  : undefined,
155
172
  }
@@ -322,7 +339,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
322
339
  await this.openConnection()
323
340
  const columnsResponse: { rows: PostgresColumn[] } =
324
341
  await this.client.query(this.COLUMNS_SQL)
325
- return columnsResponse.rows.map(row => row.table_name)
342
+ const names = columnsResponse.rows.map(row => row.table_name)
343
+ return [...new Set(names)]
326
344
  } finally {
327
345
  await this.closeConnection()
328
346
  }
@@ -103,7 +103,7 @@ export default async (ctx: UserCtx, next: any) => {
103
103
  userId,
104
104
  globalId,
105
105
  roleId,
106
- role: await roles.getRole(roleId),
106
+ role: await roles.getRole(roleId, { defaultPublic: true }),
107
107
  }
108
108
  }
109
109
 
@@ -1,4 +1,4 @@
1
- import { db as dbCore, objectStore } from "@budibase/backend-core"
1
+ import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
2
2
  import { budibaseTempDir } from "../../../utilities/budibaseDir"
3
3
  import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
4
4
  import { ObjectStoreBuckets } from "../../../constants"
@@ -18,7 +18,8 @@ import { join } from "path"
18
18
  import env from "../../../environment"
19
19
 
20
20
  const uuid = require("uuid/v4")
21
- const tar = require("tar")
21
+ import tar from "tar"
22
+
22
23
  const MemoryStream = require("memorystream")
23
24
 
24
25
  interface DBDumpOpts {
@@ -30,16 +31,18 @@ interface ExportOpts extends DBDumpOpts {
30
31
  tar?: boolean
31
32
  excludeRows?: boolean
32
33
  excludeLogs?: boolean
34
+ encryptPassword?: string
33
35
  }
34
36
 
35
37
  function tarFilesToTmp(tmpDir: string, files: string[]) {
36
- const exportFile = join(budibaseTempDir(), `${uuid()}.tar.gz`)
38
+ const fileName = `${uuid()}.tar.gz`
39
+ const exportFile = join(budibaseTempDir(), fileName)
37
40
  tar.create(
38
41
  {
39
42
  sync: true,
40
43
  gzip: true,
41
44
  file: exportFile,
42
- recursive: true,
45
+ noDirRecurse: false,
43
46
  cwd: tmpDir,
44
47
  },
45
48
  files
@@ -124,6 +127,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
124
127
  )
125
128
  }
126
129
  }
130
+
127
131
  const downloadedPath = join(tmpPath, appPath)
128
132
  if (fs.existsSync(downloadedPath)) {
129
133
  const allFiles = fs.readdirSync(downloadedPath)
@@ -141,12 +145,27 @@ export async function exportApp(appId: string, config?: ExportOpts) {
141
145
  filter: defineFilter(config?.excludeRows, config?.excludeLogs),
142
146
  exportPath: dbPath,
143
147
  })
148
+
149
+ if (config?.encryptPassword) {
150
+ for (let file of fs.readdirSync(tmpPath)) {
151
+ const path = join(tmpPath, file)
152
+
153
+ await encryption.encryptFile(
154
+ { dir: tmpPath, filename: file },
155
+ config.encryptPassword
156
+ )
157
+
158
+ fs.rmSync(path)
159
+ }
160
+ }
161
+
144
162
  // if tar requested, return where the tarball is
145
163
  if (config?.tar) {
146
164
  // now the tmpPath contains both the DB export and attachments, tar this
147
165
  const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
148
166
  // cleanup the tmp export files as tarball returned
149
167
  fs.rmSync(tmpPath, { recursive: true, force: true })
168
+
150
169
  return tarPath
151
170
  }
152
171
  // tar not requested, turn the directory where export is
@@ -161,11 +180,20 @@ export async function exportApp(appId: string, config?: ExportOpts) {
161
180
  * @param {boolean} excludeRows Flag to state whether the export should include data.
162
181
  * @returns {*} a readable stream of the backup which is written in real time
163
182
  */
164
- export async function streamExportApp(appId: string, excludeRows: boolean) {
183
+ export async function streamExportApp({
184
+ appId,
185
+ excludeRows,
186
+ encryptPassword,
187
+ }: {
188
+ appId: string
189
+ excludeRows: boolean
190
+ encryptPassword?: string
191
+ }) {
165
192
  const tmpPath = await exportApp(appId, {
166
193
  excludeRows,
167
194
  excludeLogs: true,
168
195
  tar: true,
196
+ encryptPassword,
169
197
  })
170
198
  return streamFile(tmpPath)
171
199
  }
@@ -1,4 +1,4 @@
1
- import { db as dbCore, objectStore } from "@budibase/backend-core"
1
+ import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
2
2
  import { Database, Row } from "@budibase/types"
3
3
  import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
4
4
  import { budibaseTempDir } from "../../../utilities/budibaseDir"
@@ -20,6 +20,7 @@ type TemplateType = {
20
20
  file?: {
21
21
  type: string
22
22
  path: string
23
+ password?: string
23
24
  }
24
25
  key?: string
25
26
  }
@@ -123,6 +124,22 @@ export function untarFile(file: { path: string }) {
123
124
  return tmpPath
124
125
  }
125
126
 
127
+ async function decryptFiles(path: string, password: string) {
128
+ try {
129
+ for (let file of fs.readdirSync(path)) {
130
+ const inputPath = join(path, file)
131
+ const outputPath = inputPath.replace(/\.enc$/, "")
132
+ await encryption.decryptFile(inputPath, outputPath, password)
133
+ fs.rmSync(inputPath)
134
+ }
135
+ } catch (err: any) {
136
+ if (err.message === "incorrect header check") {
137
+ throw new Error("File cannot be imported")
138
+ }
139
+ throw err
140
+ }
141
+ }
142
+
126
143
  export function getGlobalDBFile(tmpPath: string) {
127
144
  return fs.readFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), "utf8")
128
145
  }
@@ -143,6 +160,9 @@ export async function importApp(
143
160
  template.file && fs.lstatSync(template.file.path).isDirectory()
144
161
  if (template.file && (isTar || isDirectory)) {
145
162
  const tmpPath = isTar ? untarFile(template.file) : template.file.path
163
+ if (isTar && template.file.password) {
164
+ await decryptFiles(tmpPath, template.file.password)
165
+ }
146
166
  const contents = fs.readdirSync(tmpPath)
147
167
  // have to handle object import
148
168
  if (contents.length) {
@@ -164,5 +164,6 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
164
164
  delete update.config[key]
165
165
  }
166
166
  }
167
+
167
168
  return update
168
169
  }
@@ -9,7 +9,7 @@ import {
9
9
  env as coreEnv,
10
10
  } from "@budibase/backend-core"
11
11
  import { updateAppRole } from "./global"
12
- import { BBContext, User } from "@budibase/types"
12
+ import { BBContext, User, EmailInvite } from "@budibase/types"
13
13
 
14
14
  export function request(ctx?: BBContext, request?: any) {
15
15
  if (!request.headers) {
@@ -65,15 +65,25 @@ async function checkResponse(
65
65
  }
66
66
 
67
67
  // have to pass in the tenant ID as this could be coming from an automation
68
- export async function sendSmtpEmail(
69
- to: string,
70
- from: string,
71
- subject: string,
72
- contents: string,
73
- cc: string,
74
- bcc: string,
68
+ export async function sendSmtpEmail({
69
+ to,
70
+ from,
71
+ subject,
72
+ contents,
73
+ cc,
74
+ bcc,
75
+ automation,
76
+ invite,
77
+ }: {
78
+ to: string
79
+ from: string
80
+ subject: string
81
+ contents: string
82
+ cc: string
83
+ bcc: string
75
84
  automation: boolean
76
- ) {
85
+ invite?: EmailInvite
86
+ }) {
77
87
  // tenant ID will be set in header
78
88
  const response = await fetch(
79
89
  checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
@@ -88,6 +98,7 @@ export async function sendSmtpEmail(
88
98
  bcc,
89
99
  purpose: "custom",
90
100
  automation,
101
+ invite,
91
102
  },
92
103
  })
93
104
  )
@@ -1,71 +0,0 @@
1
-
2
- function generateResponse(to, from) {
3
- return {
4
- "success": true,
5
- "response": {
6
- "accepted": [
7
- to
8
- ],
9
- "envelope": {
10
- "from": from,
11
- "to": [
12
- to
13
- ]
14
- },
15
- "message": `Email sent to ${to}.`
16
- }
17
-
18
- }
19
- }
20
-
21
- const mockFetch = jest.fn(() => ({
22
- headers: {
23
- raw: () => {
24
- return { "content-type": ["application/json"] }
25
- },
26
- get: () => ["application/json"],
27
- },
28
- json: jest.fn(() => response),
29
- status: 200,
30
- text: jest.fn(),
31
- }))
32
- jest.mock("node-fetch", () => mockFetch)
33
- const setup = require("./utilities")
34
-
35
-
36
- describe("test the outgoing webhook action", () => {
37
- let inputs
38
- let config = setup.getConfig()
39
- beforeAll(async () => {
40
- await config.init()
41
- })
42
-
43
- afterAll(setup.afterAll)
44
-
45
- it("should be able to run the action", async () => {
46
- inputs = {
47
- to: "user1@test.com",
48
- from: "admin@test.com",
49
- subject: "hello",
50
- contents: "testing",
51
- }
52
- let resp = generateResponse(inputs.to, inputs.from)
53
- mockFetch.mockImplementationOnce(() => ({
54
- headers: {
55
- raw: () => {
56
- return { "content-type": ["application/json"] }
57
- },
58
- get: () => ["application/json"],
59
- },
60
- json: jest.fn(() => resp),
61
- status: 200,
62
- text: jest.fn(),
63
- }))
64
- const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
65
- expect(res.response).toEqual(resp)
66
- expect(res.success).toEqual(true)
67
-
68
- })
69
-
70
-
71
- })