mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
117 changed files with 1175 additions and 2465 deletions
@ -1 +1,12 @@ |
|||
# Budibase Authentication Library |
|||
# Budibase Core backend library |
|||
|
|||
This library contains core functionality, like auth and security features |
|||
which are shared between backend services. |
|||
|
|||
#### Note about top level JS files |
|||
For the purposes of being able to do say `require("@budibase/auth/permissions")` we need to |
|||
specify the exports at the top-level of the module. |
|||
|
|||
For these files they should be limited to a single `require` of the file that should |
|||
be exported and then a single `module.exports = ...` to export the file in |
|||
commonJS. |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/db/utils") |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/security/permissions") |
|||
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
Client: require("./src/redis"), |
|||
utils: require("./src/redis/utils"), |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/security/roles") |
|||
@ -0,0 +1,79 @@ |
|||
const { getDB } = require(".") |
|||
|
|||
class Replication { |
|||
/** |
|||
* |
|||
* @param {String} source - the DB you want to replicate or rollback to |
|||
* @param {String} target - the DB you want to replicate to, or rollback from |
|||
*/ |
|||
constructor({ source, target }) { |
|||
this.source = getDB(source) |
|||
this.target = getDB(target) |
|||
} |
|||
|
|||
promisify(operation, opts = {}) { |
|||
return new Promise(resolve => { |
|||
operation(this.target, opts) |
|||
.on("denied", function (err) { |
|||
// a document failed to replicate (e.g. due to permissions)
|
|||
throw new Error(`Denied: Document failed to replicate ${err}`) |
|||
}) |
|||
.on("complete", function (info) { |
|||
return resolve(info) |
|||
}) |
|||
.on("error", function (err) { |
|||
throw new Error(`Replication Error: ${err}`) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Two way replication operation, intended to be promise based. |
|||
* @param {Object} opts - PouchDB replication options |
|||
*/ |
|||
sync(opts = {}) { |
|||
this.replication = this.promisify(this.source.sync, opts) |
|||
return this.replication |
|||
} |
|||
|
|||
/** |
|||
* One way replication operation, intended to be promise based. |
|||
* @param {Object} opts - PouchDB replication options |
|||
*/ |
|||
replicate(opts = {}) { |
|||
this.replication = this.promisify(this.source.replicate.to, opts) |
|||
return this.replication |
|||
} |
|||
|
|||
/** |
|||
* Set up an ongoing live sync between 2 CouchDB databases. |
|||
* @param {Object} opts - PouchDB replication options |
|||
*/ |
|||
subscribe(opts = {}) { |
|||
this.replication = this.source.replicate |
|||
.to(this.target, { |
|||
live: true, |
|||
retry: true, |
|||
...opts, |
|||
}) |
|||
.on("error", function (err) { |
|||
throw new Error(`Replication Error: ${err}`) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Rollback the target DB back to the state of the source DB |
|||
*/ |
|||
async rollback() { |
|||
await this.target.destroy() |
|||
// Recreate the DB again
|
|||
this.target = getDB(this.target.name) |
|||
await this.replicate() |
|||
} |
|||
|
|||
cancel() { |
|||
this.replication.cancel() |
|||
} |
|||
} |
|||
|
|||
module.exports = Replication |
|||
@ -0,0 +1,113 @@ |
|||
<script> |
|||
import { onMount, onDestroy } from "svelte" |
|||
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import analytics from "analytics" |
|||
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte" |
|||
|
|||
const DeploymentStatus = { |
|||
SUCCESS: "SUCCESS", |
|||
PENDING: "PENDING", |
|||
FAILURE: "FAILURE", |
|||
} |
|||
|
|||
const POLL_INTERVAL = 1000 |
|||
|
|||
let loading = false |
|||
let feedbackModal |
|||
let deployments = [] |
|||
let poll |
|||
let publishModal |
|||
|
|||
$: appId = $store.appId |
|||
|
|||
async function deployApp() { |
|||
try { |
|||
notifications.info(`Deployment started. Please wait.`) |
|||
const response = await api.post("/api/deploy") |
|||
const json = await response.json() |
|||
if (response.status !== 200) { |
|||
throw new Error() |
|||
} |
|||
|
|||
if (analytics.requestFeedbackOnDeploy()) { |
|||
feedbackModal.show() |
|||
} |
|||
} catch (err) { |
|||
analytics.captureException(err) |
|||
notifications.error("Deployment unsuccessful. Please try again later.") |
|||
} |
|||
} |
|||
|
|||
async function fetchDeployments() { |
|||
try { |
|||
const response = await api.get(`/api/deployments`) |
|||
const json = await response.json() |
|||
|
|||
if (deployments.length > 0) { |
|||
checkIncomingDeploymentStatus(deployments, json) |
|||
} |
|||
|
|||
deployments = json |
|||
} catch (err) { |
|||
console.error(err) |
|||
clearInterval(poll) |
|||
notifications.error( |
|||
"Error fetching deployment history. Please try again." |
|||
) |
|||
} |
|||
} |
|||
|
|||
// Required to check any updated deployment statuses between polls |
|||
function checkIncomingDeploymentStatus(current, incoming) { |
|||
console.log(current, incoming) |
|||
for (let incomingDeployment of incoming) { |
|||
if ( |
|||
incomingDeployment.status === DeploymentStatus.FAILURE || |
|||
incomingDeployment.status === DeploymentStatus.SUCCESS |
|||
) { |
|||
const currentDeployment = current.find( |
|||
deployment => deployment._id === incomingDeployment._id |
|||
) |
|||
|
|||
// We have just been notified of an ongoing deployments status change |
|||
if ( |
|||
!currentDeployment || |
|||
currentDeployment.status === DeploymentStatus.PENDING |
|||
) { |
|||
if (incomingDeployment.status === DeploymentStatus.FAILURE) { |
|||
notifications.error(incomingDeployment.err) |
|||
} else { |
|||
notifications.send( |
|||
"Published to Production.", |
|||
"success", |
|||
"CheckmarkCircle" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
fetchDeployments() |
|||
poll = setInterval(fetchDeployments, POLL_INTERVAL) |
|||
}) |
|||
|
|||
onDestroy(() => clearInterval(poll)) |
|||
</script> |
|||
|
|||
<Button secondary on:click={publishModal.show}>Publish</Button> |
|||
<Modal bind:this={publishModal}> |
|||
<ModalContent |
|||
title="Publish to Production" |
|||
confirmText="Publish" |
|||
onConfirm={deployApp} |
|||
> |
|||
<span |
|||
>The changes you have made will be published to the production version of |
|||
the application.</span |
|||
> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -0,0 +1,50 @@ |
|||
<script> |
|||
import { onMount, onDestroy } from "svelte" |
|||
import { |
|||
Button, |
|||
Icon, |
|||
Modal, |
|||
notifications, |
|||
ModalContent, |
|||
} from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
import { apps } from "stores/portal" |
|||
import api from "builderStore/api" |
|||
|
|||
let revertModal |
|||
|
|||
$: appId = $store.appId |
|||
|
|||
const revert = async () => { |
|||
try { |
|||
const response = await api.post(`/api/dev/${appId}/revert`) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw json.message |
|||
|
|||
// Reset frontend state after revert |
|||
const applicationPkg = await api.get( |
|||
`/api/applications/${appId}/appPackage` |
|||
) |
|||
const pkg = await applicationPkg.json() |
|||
if (applicationPkg.ok) { |
|||
await store.actions.initialise(pkg) |
|||
} else { |
|||
throw new Error(pkg) |
|||
} |
|||
|
|||
notifications.info("Changes reverted.") |
|||
} catch (err) { |
|||
notifications.error(`Error reverting changes: ${err}`) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Icon name="Revert" hoverable on:click={revertModal.show} /> |
|||
<Modal bind:this={revertModal}> |
|||
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}> |
|||
<span |
|||
>The changes you have made will be deleted and the application reverted |
|||
back to its production state.</span |
|||
> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -1,2 +0,0 @@ |
|||
<!-- routify:options index=4 --> |
|||
<slot /> |
|||
@ -1,102 +0,0 @@ |
|||
<script> |
|||
import { Button, Modal, notifications, Heading } from "@budibase/bbui" |
|||
import { store, hostingStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import DeploymentHistory from "components/deploy/DeploymentHistory.svelte" |
|||
import analytics from "analytics" |
|||
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte" |
|||
import Rocket from "/assets/deploy-rocket.jpg" |
|||
|
|||
let loading = false |
|||
let deployments = [] |
|||
let poll |
|||
let feedbackModal |
|||
|
|||
$: appId = $store.appId |
|||
|
|||
async function deployApp() { |
|||
// Must have cloud or self host API key to deploy |
|||
if (!$hostingStore.hostingInfo?.selfHostKey) { |
|||
const response = await api.get(`/api/keys/`) |
|||
const userKeys = await response.json() |
|||
if (!userKeys.budibase) { |
|||
notifications.error( |
|||
"No budibase API Keys configured. You must set either a self hosted or cloud API key to deploy your budibase app." |
|||
) |
|||
return |
|||
} |
|||
} |
|||
|
|||
const DEPLOY_URL = `/api/deploy` |
|||
|
|||
try { |
|||
notifications.info(`Deployment started. Please wait.`) |
|||
const response = await api.post(DEPLOY_URL) |
|||
const json = await response.json() |
|||
if (response.status !== 200) { |
|||
throw new Error() |
|||
} |
|||
|
|||
analytics.captureEvent("Deployed App", { |
|||
appId, |
|||
hostingType: $hostingStore.hostingInfo?.type, |
|||
}) |
|||
|
|||
if (analytics.requestFeedbackOnDeploy()) { |
|||
feedbackModal.show() |
|||
} |
|||
} catch (err) { |
|||
analytics.captureEvent("Deploy App Failed", { |
|||
appId, |
|||
hostingType: $hostingStore.hostingInfo?.type, |
|||
}) |
|||
analytics.captureException(err) |
|||
notifications.error("Deployment unsuccessful. Please try again later.") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<section> |
|||
<img src={Rocket} alt="Rocket flying through sky" /> |
|||
<div> |
|||
<Heading size="M">It's time to shine!</Heading> |
|||
<Button size="XL" cta medium on:click={deployApp}>Deploy App</Button> |
|||
</div> |
|||
</section> |
|||
<Modal bind:this={feedbackModal}> |
|||
<FeedbackIframe on:finished={() => feedbackModal.hide()} /> |
|||
</Modal> |
|||
<DeploymentHistory {appId} /> |
|||
|
|||
<style> |
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
filter: brightness(80%); |
|||
} |
|||
|
|||
section { |
|||
position: relative; |
|||
min-height: 100%; |
|||
} |
|||
|
|||
div { |
|||
position: absolute; |
|||
display: flex; |
|||
text-align: center; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
left: 0; |
|||
right: 0; |
|||
top: 20%; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
width: 50%; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
div :global(h1) { |
|||
color: white; |
|||
} |
|||
</style> |
|||
@ -1,80 +0,0 @@ |
|||
const AWS = require("aws-sdk") |
|||
const fetch = require("node-fetch") |
|||
const env = require("../../../environment") |
|||
const { |
|||
deployToObjectStore, |
|||
performReplication, |
|||
fetchCredentials, |
|||
} = require("./utils") |
|||
|
|||
/** |
|||
* Verifies the users API key and |
|||
* Verifies that the deployment fits within the quota of the user |
|||
* Links to the "check-api-key" lambda. |
|||
* @param {object} deployment - information about the active deployment, including the appId and quota. |
|||
*/ |
|||
exports.preDeployment = async function (deployment) { |
|||
const json = await fetchCredentials(env.DEPLOYMENT_CREDENTIALS_URL, { |
|||
apiKey: env.BUDIBASE_API_KEY, |
|||
appId: deployment.getAppId(), |
|||
quota: deployment.getQuota(), |
|||
}) |
|||
|
|||
// set credentials here, means any time we're verified we're ready to go
|
|||
if (json.credentials) { |
|||
AWS.config.update({ |
|||
accessKeyId: json.credentials.AccessKeyId, |
|||
secretAccessKey: json.credentials.SecretAccessKey, |
|||
sessionToken: json.credentials.SessionToken, |
|||
}) |
|||
} |
|||
|
|||
return json |
|||
} |
|||
|
|||
/** |
|||
* Finalises the deployment, updating the quota for the user API key |
|||
* The verification process returns the levels to update to. |
|||
* Calls the "deployment-success" lambda. |
|||
* @param {object} deployment information about the active deployment, including the quota info. |
|||
* @returns {Promise<object>} The usage has been updated against the user API key. |
|||
*/ |
|||
exports.postDeployment = async function (deployment) { |
|||
const DEPLOYMENT_SUCCESS_URL = |
|||
env.DEPLOYMENT_CREDENTIALS_URL + "deploy/success" |
|||
|
|||
const response = await fetch(DEPLOYMENT_SUCCESS_URL, { |
|||
method: "POST", |
|||
body: JSON.stringify({ |
|||
apiKey: env.BUDIBASE_API_KEY, |
|||
quota: deployment.getQuota(), |
|||
}), |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
Accept: "application/json", |
|||
}, |
|||
}) |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(`Error updating deployment quota for API Key`) |
|||
} |
|||
|
|||
return await response.json() |
|||
} |
|||
|
|||
exports.deploy = async function (deployment) { |
|||
const appId = deployment.getAppId() |
|||
const { bucket, accountId } = deployment.getVerification() |
|||
const metadata = { accountId } |
|||
await deployToObjectStore(appId, bucket, metadata) |
|||
} |
|||
|
|||
exports.replicateDb = async function (deployment) { |
|||
const appId = deployment.getAppId() |
|||
const verification = deployment.getVerification() |
|||
return performReplication( |
|||
appId, |
|||
verification.couchDbSession, |
|||
env.DEPLOYMENT_DB_URL |
|||
) |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
const PouchDB = require("../../../db") |
|||
const { |
|||
DocumentTypes, |
|||
SEPARATOR, |
|||
UNICODE_MAX, |
|||
ViewNames, |
|||
} = require("../../../db/utils") |
|||
|
|||
exports.getAppQuota = async function (appId) { |
|||
const db = new PouchDB(appId) |
|||
|
|||
const rows = await db.allDocs({ |
|||
startkey: DocumentTypes.ROW + SEPARATOR, |
|||
endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX, |
|||
}) |
|||
|
|||
const users = await db.allDocs({ |
|||
startkey: DocumentTypes.USER + SEPARATOR, |
|||
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX, |
|||
}) |
|||
|
|||
const existingRows = rows.rows.length |
|||
const existingUsers = users.rows.length |
|||
|
|||
const designDoc = await db.get("_design/database") |
|||
|
|||
let views = 0 |
|||
for (let viewName of Object.keys(designDoc.views)) { |
|||
if (Object.values(ViewNames).indexOf(viewName) === -1) { |
|||
views++ |
|||
} |
|||
} |
|||
|
|||
return { |
|||
rows: existingRows, |
|||
users: existingUsers, |
|||
views: views, |
|||
} |
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
const AWS = require("aws-sdk") |
|||
const { |
|||
deployToObjectStore, |
|||
performReplication, |
|||
fetchCredentials, |
|||
} = require("./utils") |
|||
const { |
|||
getWorkerUrl, |
|||
getCouchUrl, |
|||
getSelfHostKey, |
|||
} = require("../../../utilities/builder/hosting") |
|||
|
|||
exports.preDeployment = async function () { |
|||
const url = `${await getWorkerUrl()}/api/deploy` |
|||
try { |
|||
const json = await fetchCredentials(url, { |
|||
selfHostKey: await getSelfHostKey(), |
|||
}) |
|||
|
|||
// response contains:
|
|||
// couchDbSession, bucket, objectStoreSession
|
|||
|
|||
// set credentials here, means any time we're verified we're ready to go
|
|||
if (json.objectStoreSession) { |
|||
AWS.config.update({ |
|||
accessKeyId: json.objectStoreSession.accessKeyId, |
|||
secretAccessKey: json.objectStoreSession.secretAccessKey, |
|||
}) |
|||
} |
|||
|
|||
return json |
|||
} catch (err) { |
|||
throw { |
|||
message: "Unauthorised to deploy, check self hosting key", |
|||
status: 401, |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.postDeployment = async function () { |
|||
// we don't actively need to do anything after deployment in self hosting
|
|||
} |
|||
|
|||
exports.deploy = async function (deployment) { |
|||
const appId = deployment.getAppId() |
|||
const verification = deployment.getVerification() |
|||
// no metadata, aws has account ID in metadata
|
|||
const metadata = {} |
|||
await deployToObjectStore(appId, verification.bucket, metadata) |
|||
} |
|||
|
|||
exports.replicateDb = async function (deployment) { |
|||
const appId = deployment.getAppId() |
|||
const verification = deployment.getVerification() |
|||
return performReplication( |
|||
appId, |
|||
verification.couchDbSession, |
|||
await getCouchUrl() |
|||
) |
|||
} |
|||
@ -1,136 +0,0 @@ |
|||
const { join } = require("../../../utilities/centralPath") |
|||
const fs = require("fs") |
|||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|||
const fetch = require("node-fetch") |
|||
const PouchDB = require("../../../db") |
|||
const CouchDB = require("pouchdb") |
|||
const { upload } = require("../../../utilities/fileSystem") |
|||
const { attachmentsRelativeURL } = require("../../../utilities") |
|||
|
|||
// TODO: everything in this file is to be removed
|
|||
|
|||
function walkDir(dirPath, callback) { |
|||
for (let filename of fs.readdirSync(dirPath)) { |
|||
const filePath = `${dirPath}/${filename}` |
|||
const stat = fs.lstatSync(filePath) |
|||
|
|||
if (stat.isFile()) { |
|||
callback(filePath) |
|||
} else { |
|||
walkDir(filePath, callback) |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.fetchCredentials = async function (url, body) { |
|||
const response = await fetch(url, { |
|||
method: "POST", |
|||
body: JSON.stringify(body), |
|||
headers: { "Content-Type": "application/json" }, |
|||
}) |
|||
|
|||
const json = await response.json() |
|||
if (json.errors) { |
|||
throw new Error(json.errors) |
|||
} |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error( |
|||
`Error fetching temporary credentials: ${JSON.stringify(json)}` |
|||
) |
|||
} |
|||
|
|||
return json |
|||
} |
|||
|
|||
exports.prepareUpload = async function ({ s3Key, bucket, metadata, file }) { |
|||
const response = await upload({ |
|||
bucket, |
|||
metadata, |
|||
filename: s3Key, |
|||
path: file.path, |
|||
type: file.type, |
|||
}) |
|||
|
|||
// don't store a URL, work this out on the way out as the URL could change
|
|||
return { |
|||
size: file.size, |
|||
name: file.name, |
|||
url: attachmentsRelativeURL(response.Key), |
|||
extension: [...file.name.split(".")].pop(), |
|||
key: response.Key, |
|||
} |
|||
} |
|||
|
|||
exports.deployToObjectStore = async function (appId, bucket, metadata) { |
|||
const appAssetsPath = join(budibaseAppsDir(), appId, "public") |
|||
|
|||
let uploads = [] |
|||
|
|||
// Upload HTML, CSS and JS for each page of the web app
|
|||
walkDir(appAssetsPath, function (filePath) { |
|||
const filePathParts = filePath.split("/") |
|||
const appAssetUpload = exports.prepareUpload({ |
|||
bucket, |
|||
file: { |
|||
path: filePath, |
|||
name: filePathParts.pop(), |
|||
}, |
|||
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`), |
|||
metadata, |
|||
}) |
|||
uploads.push(appAssetUpload) |
|||
}) |
|||
|
|||
// Upload file attachments
|
|||
const db = new PouchDB(appId) |
|||
let fileUploads |
|||
try { |
|||
fileUploads = await db.get("_local/fileuploads") |
|||
} catch (err) { |
|||
fileUploads = { _id: "_local/fileuploads", uploads: [] } |
|||
} |
|||
|
|||
for (let file of fileUploads.uploads) { |
|||
if (file.uploaded) continue |
|||
|
|||
const attachmentUpload = exports.prepareUpload({ |
|||
file, |
|||
s3Key: `assets/${appId}/attachments/${file.processedFileName}`, |
|||
bucket, |
|||
metadata, |
|||
}) |
|||
|
|||
uploads.push(attachmentUpload) |
|||
|
|||
// mark file as uploaded
|
|||
file.uploaded = true |
|||
} |
|||
|
|||
db.put(fileUploads) |
|||
|
|||
try { |
|||
return await Promise.all(uploads) |
|||
} catch (err) { |
|||
console.error("Error uploading budibase app assets to s3", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
exports.performReplication = (appId, session, dbUrl) => { |
|||
return new Promise((resolve, reject) => { |
|||
const local = new PouchDB(appId) |
|||
|
|||
const remote = new CouchDB(`${dbUrl}/${appId}`, { |
|||
fetch: function (url, opts) { |
|||
opts.headers.set("Cookie", `${session};`) |
|||
return CouchDB.fetch(url, opts) |
|||
}, |
|||
}) |
|||
|
|||
const replication = local.sync(remote) |
|||
|
|||
replication.on("complete", () => resolve()) |
|||
replication.on("error", err => reject(err)) |
|||
}) |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue