mirror of https://github.com/Budibase/budibase.git
85 changed files with 3148 additions and 1132 deletions
@ -0,0 +1 @@ |
|||||
|
hosting.properties |
||||
@ -0,0 +1,16 @@ |
|||||
|
version: "3" |
||||
|
|
||||
|
services: |
||||
|
app-service: |
||||
|
build: ./server |
||||
|
volumes: |
||||
|
- ./server:/app |
||||
|
environment: |
||||
|
SELF_HOSTED: 1 |
||||
|
PORT: 4002 |
||||
|
|
||||
|
worker-service: |
||||
|
build: ./worker |
||||
|
environment: |
||||
|
SELF_HOSTED: 1, |
||||
|
PORT: 4003 |
||||
@ -0,0 +1 @@ |
|||||
|
../../packages/server/ |
||||
@ -0,0 +1 @@ |
|||||
|
../../packages/worker/ |
||||
@ -0,0 +1,90 @@ |
|||||
|
version: "3" |
||||
|
|
||||
|
services: |
||||
|
app-service: |
||||
|
image: budibase/budibase-apps |
||||
|
ports: |
||||
|
- "${APP_PORT}:4002" |
||||
|
environment: |
||||
|
SELF_HOSTED: 1 |
||||
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984 |
||||
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} |
||||
|
LOGO_URL: ${LOGO_URL} |
||||
|
PORT: 4002 |
||||
|
JWT_SECRET: ${JWT_SECRET} |
||||
|
depends_on: |
||||
|
- worker-service |
||||
|
|
||||
|
worker-service: |
||||
|
image: budibase/budibase-worker |
||||
|
ports: |
||||
|
- "${WORKER_PORT}:4003" |
||||
|
environment: |
||||
|
SELF_HOSTED: 1, |
||||
|
DEPLOYMENT_API_KEY: ${WORKER_API_KEY} |
||||
|
PORT: 4003 |
||||
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} |
||||
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} |
||||
|
RAW_MINIO_URL: http://minio-service:9000 |
||||
|
COUCH_DB_USERNAME: ${COUCH_DB_USER} |
||||
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} |
||||
|
RAW_COUCH_DB_URL: http://couchdb-service:5984 |
||||
|
SELF_HOST_KEY: ${HOSTING_KEY} |
||||
|
depends_on: |
||||
|
- minio-service |
||||
|
- couch-init |
||||
|
|
||||
|
minio-service: |
||||
|
image: minio/minio |
||||
|
volumes: |
||||
|
- minio_data:/data |
||||
|
ports: |
||||
|
- "${MINIO_PORT}:9000" |
||||
|
environment: |
||||
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} |
||||
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} |
||||
|
command: server /data |
||||
|
healthcheck: |
||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] |
||||
|
interval: 30s |
||||
|
timeout: 20s |
||||
|
retries: 3 |
||||
|
|
||||
|
proxy-service: |
||||
|
image: envoyproxy/envoy:v1.16-latest |
||||
|
volumes: |
||||
|
- ./envoy.yaml:/etc/envoy/envoy.yaml |
||||
|
ports: |
||||
|
- "${MAIN_PORT}:10000" |
||||
|
- "9901:9901" |
||||
|
depends_on: |
||||
|
- minio-service |
||||
|
- worker-service |
||||
|
- app-service |
||||
|
- couchdb-service |
||||
|
|
||||
|
couchdb-service: |
||||
|
image: apache/couchdb:3.0 |
||||
|
environment: |
||||
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} |
||||
|
- COUCHDB_USER=${COUCH_DB_USER} |
||||
|
ports: |
||||
|
- "${COUCH_DB_PORT}:5984" |
||||
|
- "4369:4369" |
||||
|
- "9100:9100" |
||||
|
volumes: |
||||
|
- couchdb_data:/couchdb |
||||
|
|
||||
|
couch-init: |
||||
|
image: curlimages/curl |
||||
|
environment: |
||||
|
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984" |
||||
|
depends_on: |
||||
|
- couchdb-service |
||||
|
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"] |
||||
|
|
||||
|
volumes: |
||||
|
couchdb_data: |
||||
|
driver: local |
||||
|
minio_data: |
||||
|
driver: local |
||||
@ -0,0 +1,104 @@ |
|||||
|
static_resources: |
||||
|
listeners: |
||||
|
- name: main_listener |
||||
|
address: |
||||
|
socket_address: { address: 0.0.0.0, port_value: 10000 } |
||||
|
filter_chains: |
||||
|
- filters: |
||||
|
- name: envoy.filters.network.http_connection_manager |
||||
|
typed_config: |
||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager |
||||
|
stat_prefix: ingress |
||||
|
codec_type: auto |
||||
|
route_config: |
||||
|
name: local_route |
||||
|
virtual_hosts: |
||||
|
- name: local_services |
||||
|
domains: ["*"] |
||||
|
routes: |
||||
|
- match: { prefix: "/app/" } |
||||
|
route: |
||||
|
cluster: app-service |
||||
|
prefix_rewrite: "/" |
||||
|
|
||||
|
# special case for when API requests are made, can just forward, not to minio |
||||
|
- match: { prefix: "/api/" } |
||||
|
route: |
||||
|
cluster: app-service |
||||
|
|
||||
|
- match: { prefix: "/worker/" } |
||||
|
route: |
||||
|
cluster: worker-service |
||||
|
prefix_rewrite: "/" |
||||
|
|
||||
|
- match: { prefix: "/db/" } |
||||
|
route: |
||||
|
cluster: couchdb-service |
||||
|
prefix_rewrite: "/" |
||||
|
|
||||
|
# minio is on the default route because this works |
||||
|
# best, minio + AWS SDK doesn't handle path proxy |
||||
|
- match: { prefix: "/" } |
||||
|
route: |
||||
|
cluster: minio-service |
||||
|
|
||||
|
http_filters: |
||||
|
- name: envoy.filters.http.router |
||||
|
|
||||
|
clusters: |
||||
|
- name: app-service |
||||
|
connect_timeout: 0.25s |
||||
|
type: strict_dns |
||||
|
lb_policy: round_robin |
||||
|
load_assignment: |
||||
|
cluster_name: app-service |
||||
|
endpoints: |
||||
|
- lb_endpoints: |
||||
|
- endpoint: |
||||
|
address: |
||||
|
socket_address: |
||||
|
address: app-service |
||||
|
port_value: 4002 |
||||
|
|
||||
|
- name: minio-service |
||||
|
connect_timeout: 0.25s |
||||
|
type: strict_dns |
||||
|
lb_policy: round_robin |
||||
|
load_assignment: |
||||
|
cluster_name: minio-service |
||||
|
endpoints: |
||||
|
- lb_endpoints: |
||||
|
- endpoint: |
||||
|
address: |
||||
|
socket_address: |
||||
|
address: minio-service |
||||
|
port_value: 9000 |
||||
|
|
||||
|
- name: worker-service |
||||
|
connect_timeout: 0.25s |
||||
|
type: strict_dns |
||||
|
lb_policy: round_robin |
||||
|
load_assignment: |
||||
|
cluster_name: worker-service |
||||
|
endpoints: |
||||
|
- lb_endpoints: |
||||
|
- endpoint: |
||||
|
address: |
||||
|
socket_address: |
||||
|
address: worker-service |
||||
|
port_value: 4003 |
||||
|
|
||||
|
- name: couchdb-service |
||||
|
connect_timeout: 0.25s |
||||
|
type: strict_dns |
||||
|
lb_policy: round_robin |
||||
|
load_assignment: |
||||
|
cluster_name: couchdb-service |
||||
|
endpoints: |
||||
|
- lb_endpoints: |
||||
|
- endpoint: |
||||
|
address: |
||||
|
socket_address: |
||||
|
address: couchdb-service |
||||
|
port_value: 5984 |
||||
|
|
||||
@ -0,0 +1,25 @@ |
|||||
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000 |
||||
|
MAIN_PORT=10000 |
||||
|
|
||||
|
# Use this password when configuring your self hosting settings |
||||
|
# This should be updated |
||||
|
HOSTING_KEY=budibase |
||||
|
|
||||
|
# This section contains customisation options |
||||
|
LOGO_URL=https://logoipsum.com/logo/logo-15.svg |
||||
|
|
||||
|
# This section contains all secrets pertaining to the system |
||||
|
# These should be updated |
||||
|
JWT_SECRET=testsecret |
||||
|
MINIO_ACCESS_KEY=budibase |
||||
|
MINIO_SECRET_KEY=budibase |
||||
|
COUCH_DB_PASSWORD=budibase |
||||
|
COUCH_DB_USER=budibase |
||||
|
WORKER_API_KEY=budibase |
||||
|
|
||||
|
# This section contains variables that do not need to be altered under normal circumstances |
||||
|
APP_PORT=4002 |
||||
|
WORKER_PORT=4003 |
||||
|
MINIO_PORT=4004 |
||||
|
COUCH_DB_PORT=4005 |
||||
|
BUDIBASE_ENVIRONMENT=PRODUCTION |
||||
@ -0,0 +1,4 @@ |
|||||
|
#!/bin/bash |
||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose |
||||
|
sudo chmod +x /usr/local/bin/docker-compose |
||||
|
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose |
||||
@ -0,0 +1,6 @@ |
|||||
|
#!/bin/bash |
||||
|
echo "**** WARNING - not for production environments ****" |
||||
|
# warning this is a convience script, for production installations install docker |
||||
|
# properly for your environment! |
||||
|
curl -fsSL https://get.docker.com -o get-docker.sh |
||||
|
sudo sh get-docker.sh |
||||
@ -0,0 +1,9 @@ |
|||||
|
#!/bin/bash |
||||
|
pushd ../../build |
||||
|
docker-compose build --force app-service |
||||
|
docker-compose build --force worker-service |
||||
|
docker tag build_app-service budibase/budibase-apps:latest |
||||
|
docker push budibase/budibase-apps |
||||
|
docker tag build_worker-service budibase/budibase-worker:latest |
||||
|
docker push budibase/budibase-worker |
||||
|
popd |
||||
@ -0,0 +1,2 @@ |
|||||
|
#!/bin/bash |
||||
|
docker-compose --env-file hosting.properties up |
||||
@ -0,0 +1,20 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
function dockerInstalled { |
||||
|
echo "Checking docker installation..." |
||||
|
if [ ! -x "$(command -v docker)" ]; then |
||||
|
echo "Please install docker to continue" |
||||
|
exit -1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
dockerInstalled |
||||
|
|
||||
|
source "${BASH_SOURCE%/*}/hosting.properties" |
||||
|
|
||||
|
opts="-e MINIO_ACCESS_KEY=$minio_access_key -e MINIO_SECRET_KEY=$minio_secret_key" |
||||
|
if [ -n "$minio_secret_key_old" ] && [ -n "$minio_access_key_old" ]; then |
||||
|
opts="$opts -e MINIO_SECRET_KEY_OLD=$minio_secret_key_old -e MINIO_ACCESS_KEY_OLD=$minio_access_key_old" |
||||
|
fi |
||||
|
|
||||
|
docker run -p $minio_port:$minio_port $opts -v /mnt/data:/data minio/minio server /data |
||||
@ -0,0 +1,38 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "../api" |
||||
|
|
||||
|
const INITIAL_BACKEND_UI_STATE = { |
||||
|
hostingInfo: {}, |
||||
|
appUrl: "", |
||||
|
} |
||||
|
|
||||
|
export const getHostingStore = () => { |
||||
|
const store = writable({ ...INITIAL_BACKEND_UI_STATE }) |
||||
|
store.actions = { |
||||
|
fetch: async () => { |
||||
|
const responses = await Promise.all([ |
||||
|
api.get("/api/hosting/"), |
||||
|
api.get("/api/hosting/urls"), |
||||
|
]) |
||||
|
const [info, urls] = await Promise.all(responses.map(resp => resp.json())) |
||||
|
store.update(state => { |
||||
|
state.hostingInfo = info |
||||
|
state.appUrl = urls.app |
||||
|
return state |
||||
|
}) |
||||
|
return info |
||||
|
}, |
||||
|
save: async hostingInfo => { |
||||
|
const response = await api.post("/api/hosting", hostingInfo) |
||||
|
const revision = (await response.json()).rev |
||||
|
store.update(state => { |
||||
|
state.hostingInfo = { |
||||
|
...hostingInfo, |
||||
|
_rev: revision, |
||||
|
} |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
} |
||||
|
return store |
||||
|
} |
||||
@ -1,41 +0,0 @@ |
|||||
<script> |
|
||||
import { |
|
||||
DropdownMenu, |
|
||||
TextButton as Button, |
|
||||
Icon, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
} from "@budibase/bbui" |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import api from "builderStore/api" |
|
||||
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte" |
|
||||
|
|
||||
export let table |
|
||||
|
|
||||
let modal |
|
||||
|
|
||||
// TODO: revisit |
|
||||
async function saveTable() { |
|
||||
const SAVE_TABLE_URL = `/api/tables` |
|
||||
const response = await api.post(SAVE_TABLE_URL, table) |
|
||||
const savedTable = await response.json() |
|
||||
await backendUiStore.actions.tables.fetch() |
|
||||
backendUiStore.actions.tables.select(savedTable) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div> |
|
||||
<Button text small on:click={modal.show}> |
|
||||
<Icon name="edit" /> |
|
||||
Configure Schema |
|
||||
</Button> |
|
||||
</div> |
|
||||
<Modal bind:this={modal}> |
|
||||
<ModalContent |
|
||||
confirmText="Save" |
|
||||
cancelText="Cancel" |
|
||||
onConfirm={saveTable} |
|
||||
title={'Datasource Configuration'}> |
|
||||
<EditIntegrationConfig onClosed={modal.hide} bind:table /> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
@ -1,129 +0,0 @@ |
|||||
<script> |
|
||||
import { |
|
||||
Select, |
|
||||
Button, |
|
||||
Input, |
|
||||
TextArea, |
|
||||
Heading, |
|
||||
Spacer, |
|
||||
} from "@budibase/bbui" |
|
||||
import { notifier } from "builderStore/store/notifications" |
|
||||
import { FIELDS } from "constants/backend" |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import * as api from "../api" |
|
||||
|
|
||||
export let table |
|
||||
|
|
||||
let smartSchemaRow |
|
||||
let fields = Object.keys(table.schema).map(field => ({ |
|
||||
name: field, |
|
||||
type: table.schema[field].type.toUpperCase(), |
|
||||
})) |
|
||||
|
|
||||
$: { |
|
||||
const schema = {} |
|
||||
for (let field of fields) { |
|
||||
if (!field.name) continue |
|
||||
schema[field.name] = FIELDS[field.type] |
|
||||
} |
|
||||
table.schema = schema |
|
||||
} |
|
||||
|
|
||||
function newField() { |
|
||||
fields = [...fields, {}] |
|
||||
} |
|
||||
|
|
||||
function deleteField(idx) { |
|
||||
fields.splice(idx, 1) |
|
||||
fields = fields |
|
||||
} |
|
||||
|
|
||||
async function smartSchema() { |
|
||||
try { |
|
||||
const rows = await api.fetchDataForView($backendUiStore.selectedView) |
|
||||
const first = rows[0] |
|
||||
smartSchemaRow = first |
|
||||
fields = Object.keys(first).map(key => ({ |
|
||||
// TODO: Smarter type mapping |
|
||||
name: key, |
|
||||
type: "STRING", |
|
||||
})) |
|
||||
} catch (err) { |
|
||||
notifier.danger("Error determining schema. Please enter fields manually.") |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<section> |
|
||||
<div class="config"> |
|
||||
<h6>Schema</h6> |
|
||||
{#if smartSchemaRow} |
|
||||
<pre>{JSON.stringify(smartSchemaRow, undefined, 2)}</pre> |
|
||||
{/if} |
|
||||
{#each fields as field, idx} |
|
||||
<div class="field"> |
|
||||
<Input thin type={'text'} bind:value={field.name} /> |
|
||||
<Select secondary thin bind:value={field.type}> |
|
||||
<option value={''}>Select an option</option> |
|
||||
<option value={'STRING'}>Text</option> |
|
||||
<option value={'NUMBER'}>Number</option> |
|
||||
<option value={'BOOLEAN'}>Boolean</option> |
|
||||
<option value={'DATETIME'}>Datetime</option> |
|
||||
</Select> |
|
||||
<i |
|
||||
class="ri-close-circle-line delete" |
|
||||
on:click={() => deleteField(idx)} /> |
|
||||
</div> |
|
||||
{/each} |
|
||||
<Button thin secondary on:click={newField}>Add Field</Button> |
|
||||
<Button thin primary on:click={smartSchema}>Smart Schema</Button> |
|
||||
</div> |
|
||||
|
|
||||
<div class="config"> |
|
||||
<h6>Datasource</h6> |
|
||||
{#each Object.keys(table.integration) as configKey} |
|
||||
{#if configKey === 'query'} |
|
||||
<TextArea |
|
||||
thin |
|
||||
label={configKey} |
|
||||
bind:value={table.integration[configKey]} /> |
|
||||
{:else} |
|
||||
<Input |
|
||||
thin |
|
||||
type={configKey.type} |
|
||||
label={configKey} |
|
||||
bind:value={table.integration[configKey]} /> |
|
||||
{/if} |
|
||||
<Spacer small /> |
|
||||
{/each} |
|
||||
</div> |
|
||||
</section> |
|
||||
|
|
||||
<style> |
|
||||
.field { |
|
||||
display: grid; |
|
||||
grid-gap: 10px; |
|
||||
grid-template-columns: 1fr 1fr 50px; |
|
||||
margin-bottom: var(--spacing-m); |
|
||||
} |
|
||||
|
|
||||
h6 { |
|
||||
font-family: var(--font-sans); |
|
||||
font-weight: 600; |
|
||||
text-rendering: var(--text-render); |
|
||||
color: var(--ink); |
|
||||
font-size: var(--heading-font-size-xs); |
|
||||
color: var(--ink); |
|
||||
margin-bottom: var(--spacing-m); |
|
||||
margin-top: var(--spacing-l); |
|
||||
} |
|
||||
|
|
||||
.config { |
|
||||
margin-bottom: var(--spacing-s); |
|
||||
} |
|
||||
|
|
||||
.delete { |
|
||||
align-self: center; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,55 @@ |
|||||
|
<script> |
||||
|
import { Label, DropdownMenu } from "@budibase/bbui" |
||||
|
import ThemeEditor from "./ThemeEditor.svelte" |
||||
|
|
||||
|
let anchor |
||||
|
let popover |
||||
|
</script> |
||||
|
|
||||
|
<div class="topnavitemright" on:click={popover.show} bind:this={anchor}> |
||||
|
<i class="ri-paint-fill" /> |
||||
|
</div> |
||||
|
<div class="dropdown"> |
||||
|
<DropdownMenu bind:this={popover} {anchor} align="right"> |
||||
|
<div class="content"> |
||||
|
<Label extraSmall grey>Theme</Label> |
||||
|
<ThemeEditor /> |
||||
|
</div> |
||||
|
</DropdownMenu> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.dropdown { |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
i { |
||||
|
font-size: 18px; |
||||
|
color: var(--grey-7); |
||||
|
} |
||||
|
.topnavitemright { |
||||
|
cursor: pointer; |
||||
|
color: var(--grey-7); |
||||
|
margin: 0 12px 0 0; |
||||
|
font-weight: 500; |
||||
|
font-size: 1rem; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
height: 24px; |
||||
|
width: 24px; |
||||
|
} |
||||
|
.topnavitemright:hover i { |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
h5 { |
||||
|
margin: 0; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
padding: var(--spacing-xl); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,32 @@ |
|||||
|
<script> |
||||
|
import { TextButton as Button, Modal } from "@budibase/bbui" |
||||
|
import BuilderSettingsModal from "./BuilderSettingsModal.svelte" |
||||
|
|
||||
|
let modal |
||||
|
</script> |
||||
|
|
||||
|
<div> |
||||
|
<Button text on:click={modal.show}> |
||||
|
<i class="ri-settings-3-fill" /> |
||||
|
<p>Settings</p> |
||||
|
</Button> |
||||
|
</div> |
||||
|
<Modal bind:this={modal} width="30%"> |
||||
|
<BuilderSettingsModal /> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
div i { |
||||
|
font-size: 26px; |
||||
|
color: var(--grey-7); |
||||
|
margin-left: 12px; |
||||
|
} |
||||
|
|
||||
|
div p { |
||||
|
font-family: var(--font-sans); |
||||
|
font-size: var(--font-size-s); |
||||
|
color: var(--ink); |
||||
|
font-weight: 400; |
||||
|
margin: 0 0 0 12px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,72 @@ |
|||||
|
<script> |
||||
|
import { notifier } from "builderStore/store/notifications" |
||||
|
import { hostingStore } from "builderStore" |
||||
|
import { Input, ModalContent, Toggle } from "@budibase/bbui" |
||||
|
import ThemeEditor from "components/settings/ThemeEditor.svelte" |
||||
|
import analytics from "analytics" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
let hostingInfo |
||||
|
let selfhosted = false |
||||
|
|
||||
|
async function save() { |
||||
|
hostingInfo.type = selfhosted ? "self" : "cloud" |
||||
|
if (!selfhosted && hostingInfo._rev) { |
||||
|
hostingInfo = { |
||||
|
type: hostingInfo.type, |
||||
|
_id: hostingInfo._id, |
||||
|
_rev: hostingInfo._rev, |
||||
|
} |
||||
|
} |
||||
|
try { |
||||
|
await hostingStore.actions.save(hostingInfo) |
||||
|
notifier.success(`Settings saved.`) |
||||
|
} catch (err) { |
||||
|
notifier.danger(`Failed to update builder settings.`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function updateSelfHosting(event) { |
||||
|
if (hostingInfo.type === "cloud" && event.target.checked) { |
||||
|
hostingInfo.hostingUrl = "localhost:10000" |
||||
|
hostingInfo.useHttps = false |
||||
|
hostingInfo.selfHostKey = "budibase" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
hostingInfo = await hostingStore.actions.fetch() |
||||
|
selfhosted = hostingInfo.type === "self" |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent title="Builder settings" confirmText="Save" onConfirm={save}> |
||||
|
<h5>Theme</h5> |
||||
|
<ThemeEditor /> |
||||
|
<h5>Hosting</h5> |
||||
|
<p> |
||||
|
This section contains settings that relate to the deployment and hosting of |
||||
|
apps made in this builder. |
||||
|
</p> |
||||
|
<Toggle |
||||
|
thin |
||||
|
text="Self hosted" |
||||
|
on:change={updateSelfHosting} |
||||
|
bind:checked={selfhosted} /> |
||||
|
{#if selfhosted} |
||||
|
<Input bind:value={hostingInfo.hostingUrl} label="Hosting URL" /> |
||||
|
<Input bind:value={hostingInfo.selfHostKey} label="Hosting Key" /> |
||||
|
<Toggle thin text="HTTPS" bind:checked={hostingInfo.useHttps} /> |
||||
|
{/if} |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
h5 { |
||||
|
margin: 0; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
p { |
||||
|
margin: 0; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,47 +0,0 @@ |
|||||
const COOKIE_SEPARATOR = ";" |
|
||||
const APP_PREFIX = "app_" |
|
||||
const KEY_VALUE_SPLIT = "=" |
|
||||
|
|
||||
function confirmAppId(possibleAppId) { |
|
||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX) |
|
||||
? possibleAppId |
|
||||
: undefined |
|
||||
} |
|
||||
|
|
||||
function tryGetFromCookie({ cookies }) { |
|
||||
if (!cookies) { |
|
||||
return undefined |
|
||||
} |
|
||||
const cookie = cookies |
|
||||
.split(COOKIE_SEPARATOR) |
|
||||
.find(cookie => cookie.trim().startsWith("budibase:currentapp")) |
|
||||
let appId |
|
||||
if (cookie && cookie.split(KEY_VALUE_SPLIT).length === 2) { |
|
||||
appId = cookie.split("=")[1] |
|
||||
} |
|
||||
return confirmAppId(appId) |
|
||||
} |
|
||||
|
|
||||
function tryGetFromPath() { |
|
||||
const appId = location.pathname.split("/")[1] |
|
||||
return confirmAppId(appId) |
|
||||
} |
|
||||
|
|
||||
function tryGetFromSubdomain() { |
|
||||
const parts = window.location.host.split(".") |
|
||||
const appId = parts[1] ? parts[0] : undefined |
|
||||
return confirmAppId(appId) |
|
||||
} |
|
||||
|
|
||||
export const getAppId = (cookies = window.document.cookie) => { |
|
||||
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie] |
|
||||
// try getting the app Id in order
|
|
||||
let appId |
|
||||
for (let func of functions) { |
|
||||
appId = func({ cookies }) |
|
||||
if (appId) { |
|
||||
break |
|
||||
} |
|
||||
} |
|
||||
return appId |
|
||||
} |
|
||||
@ -1,56 +0,0 @@ |
|||||
// THIS will create API Keys and App Ids input in a local Dynamo instance if it is running
|
|
||||
const dynamoClient = require("../src/db/dynamoClient") |
|
||||
const env = require("../src/environment") |
|
||||
|
|
||||
if (process.argv[2] == null || process.argv[3] == null) { |
|
||||
console.error( |
|
||||
"Inputs incorrect format, was expecting: node createApiKeyAndAppId.js <API_KEY> <APP_ID>" |
|
||||
) |
|
||||
process.exit(-1) |
|
||||
} |
|
||||
|
|
||||
const FAKE_STRING = "fakestring" |
|
||||
|
|
||||
// set fake credentials for local dynamo to actually work
|
|
||||
env._set("AWS_ACCESS_KEY_ID", "KEY_ID") |
|
||||
env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY") |
|
||||
dynamoClient.init("http://localhost:8333") |
|
||||
|
|
||||
async function run() { |
|
||||
await dynamoClient.apiKeyTable.put({ |
|
||||
item: { |
|
||||
pk: process.argv[2], |
|
||||
accountId: FAKE_STRING, |
|
||||
trackingId: FAKE_STRING, |
|
||||
quotaReset: Date.now() + 2592000000, |
|
||||
usageQuota: { |
|
||||
automationRuns: 0, |
|
||||
rows: 0, |
|
||||
storage: 0, |
|
||||
users: 0, |
|
||||
views: 0, |
|
||||
}, |
|
||||
usageLimits: { |
|
||||
automationRuns: 10, |
|
||||
rows: 10, |
|
||||
storage: 1000, |
|
||||
users: 10, |
|
||||
views: 10, |
|
||||
}, |
|
||||
}, |
|
||||
}) |
|
||||
await dynamoClient.apiKeyTable.put({ |
|
||||
item: { |
|
||||
pk: process.argv[3], |
|
||||
apiKey: process.argv[2], |
|
||||
}, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
run() |
|
||||
.then(() => { |
|
||||
console.log("Rows should have been created.") |
|
||||
}) |
|
||||
.catch(err => { |
|
||||
console.error("Cannot create rows - " + err) |
|
||||
}) |
|
||||
@ -0,0 +1,88 @@ |
|||||
|
const { getAppQuota } = require("./quota") |
||||
|
const env = require("../../../environment") |
||||
|
const newid = require("../../../db/newid") |
||||
|
|
||||
|
/** |
||||
|
* This is used to pass around information about the deployment that is occurring |
||||
|
*/ |
||||
|
class Deployment { |
||||
|
constructor(appId, id = null) { |
||||
|
this.appId = appId |
||||
|
this._id = id || newid() |
||||
|
} |
||||
|
|
||||
|
// purely so that we can do quota stuff outside the main deployment context
|
||||
|
async init() { |
||||
|
if (!env.SELF_HOSTED) { |
||||
|
this.setQuota(await getAppQuota(this.appId)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setQuota(quota) { |
||||
|
if (!quota) { |
||||
|
return |
||||
|
} |
||||
|
this.quota = quota |
||||
|
} |
||||
|
|
||||
|
getQuota() { |
||||
|
return this.quota |
||||
|
} |
||||
|
|
||||
|
getAppId() { |
||||
|
return this.appId |
||||
|
} |
||||
|
|
||||
|
setVerification(verification) { |
||||
|
if (!verification) { |
||||
|
return |
||||
|
} |
||||
|
this.verification = verification |
||||
|
if (this.verification.quota) { |
||||
|
this.quota = this.verification.quota |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getVerification() { |
||||
|
return this.verification |
||||
|
} |
||||
|
|
||||
|
setStatus(status, err = null) { |
||||
|
this.status = status |
||||
|
if (err) { |
||||
|
this.err = err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fromJSON(json) { |
||||
|
if (json.verification) { |
||||
|
this.setVerification(json.verification) |
||||
|
} |
||||
|
if (json.quota) { |
||||
|
this.setQuota(json.quota) |
||||
|
} |
||||
|
if (json.status) { |
||||
|
this.setStatus(json.status, json.err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getJSON() { |
||||
|
const obj = { |
||||
|
_id: this._id, |
||||
|
appId: this.appId, |
||||
|
status: this.status, |
||||
|
} |
||||
|
if (this.err) { |
||||
|
obj.err = this.err |
||||
|
} |
||||
|
if (this.verification && this.verification.cfDistribution) { |
||||
|
obj.cfDistribution = this.verification.cfDistribution |
||||
|
} |
||||
|
if (this.quota) { |
||||
|
obj.quota = this.quota |
||||
|
} |
||||
|
return obj |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = Deployment |
||||
@ -1,189 +0,0 @@ |
|||||
const fs = require("fs") |
|
||||
const { join } = require("../../../utilities/centralPath") |
|
||||
const AWS = require("aws-sdk") |
|
||||
const fetch = require("node-fetch") |
|
||||
const sanitize = require("sanitize-s3-objectkey") |
|
||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|
||||
const PouchDB = require("../../../db") |
|
||||
const env = require("../../../environment") |
|
||||
|
|
||||
/** |
|
||||
* 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} quota The usage quota levels returned from the verifyDeploy |
|
||||
* @returns {Promise<object>} The usage has been updated against the user API key. |
|
||||
*/ |
|
||||
exports.updateDeploymentQuota = async function(quota) { |
|
||||
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, |
|
||||
}), |
|
||||
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() |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 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 {String} appId - appId being deployed |
|
||||
* @param {String} appId - appId being deployed |
|
||||
* @param {quota} quota - current quota being changed with this application |
|
||||
*/ |
|
||||
exports.verifyDeployment = async function({ appId, quota }) { |
|
||||
const response = await fetch(env.DEPLOYMENT_CREDENTIALS_URL, { |
|
||||
method: "POST", |
|
||||
body: JSON.stringify({ |
|
||||
apiKey: env.BUDIBASE_API_KEY, |
|
||||
appId, |
|
||||
quota, |
|
||||
}), |
|
||||
}) |
|
||||
|
|
||||
const json = await response.json() |
|
||||
if (json.errors) { |
|
||||
throw new Error(json.errors) |
|
||||
} |
|
||||
|
|
||||
if (response.status !== 200) { |
|
||||
throw new Error( |
|
||||
`Error fetching temporary credentials for api key: ${env.BUDIBASE_API_KEY}` |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// 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 |
|
||||
} |
|
||||
|
|
||||
const CONTENT_TYPE_MAP = { |
|
||||
html: "text/html", |
|
||||
css: "text/css", |
|
||||
js: "application/javascript", |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Recursively walk a directory tree and execute a callback on all files. |
|
||||
* @param {String} dirPath - Directory to traverse |
|
||||
* @param {Function} callback - callback to execute on files |
|
||||
*/ |
|
||||
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) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async function prepareUploadForS3({ s3Key, metadata, s3, file }) { |
|
||||
const extension = [...file.name.split(".")].pop() |
|
||||
const fileBytes = fs.readFileSync(file.path) |
|
||||
|
|
||||
const upload = await s3 |
|
||||
.upload({ |
|
||||
// windows filepaths need to be converted to forward slashes for s3
|
|
||||
Key: sanitize(s3Key).replace(/\\/g, "/"), |
|
||||
Body: fileBytes, |
|
||||
ContentType: file.type || CONTENT_TYPE_MAP[extension.toLowerCase()], |
|
||||
Metadata: metadata, |
|
||||
}) |
|
||||
.promise() |
|
||||
|
|
||||
return { |
|
||||
size: file.size, |
|
||||
name: file.name, |
|
||||
extension, |
|
||||
url: upload.Location, |
|
||||
key: upload.Key, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.prepareUploadForS3 = prepareUploadForS3 |
|
||||
|
|
||||
exports.uploadAppAssets = async function({ appId, bucket, accountId }) { |
|
||||
const s3 = new AWS.S3({ |
|
||||
params: { |
|
||||
Bucket: bucket, |
|
||||
}, |
|
||||
}) |
|
||||
|
|
||||
const appAssetsPath = join(budibaseAppsDir(), appId, "public") |
|
||||
|
|
||||
let uploads = [] |
|
||||
|
|
||||
// Upload HTML and JS of the web app
|
|
||||
walkDir(appAssetsPath, function(filePath) { |
|
||||
const filePathParts = filePath.split("/") |
|
||||
const appAssetUpload = prepareUploadForS3({ |
|
||||
file: { |
|
||||
path: filePath, |
|
||||
name: filePathParts.pop(), |
|
||||
}, |
|
||||
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`), |
|
||||
s3, |
|
||||
metadata: { accountId }, |
|
||||
}) |
|
||||
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 = prepareUploadForS3({ |
|
||||
file, |
|
||||
s3Key: `assets/${appId}/attachments/${file.processedFileName}`, |
|
||||
s3, |
|
||||
metadata: { accountId }, |
|
||||
}) |
|
||||
|
|
||||
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 |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,85 @@ |
|||||
|
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 } |
||||
|
const s3Client = new AWS.S3({ |
||||
|
params: { |
||||
|
Bucket: bucket, |
||||
|
}, |
||||
|
}) |
||||
|
await deployToObjectStore(appId, s3Client, metadata) |
||||
|
} |
||||
|
|
||||
|
exports.replicateDb = async function(deployment) { |
||||
|
const appId = deployment.getAppId() |
||||
|
const verification = deployment.getVerification() |
||||
|
return performReplication( |
||||
|
appId, |
||||
|
verification.couchDbSession, |
||||
|
env.DEPLOYMENT_DB_URL |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
const PouchDB = require("../../../db") |
||||
|
const { DocumentTypes, SEPARATOR, UNICODE_MAX } = 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") |
||||
|
|
||||
|
return { |
||||
|
rows: existingRows, |
||||
|
users: existingUsers, |
||||
|
views: Object.keys(designDoc.views).length, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
const AWS = require("aws-sdk") |
||||
|
const { |
||||
|
deployToObjectStore, |
||||
|
performReplication, |
||||
|
fetchCredentials, |
||||
|
} = require("./utils") |
||||
|
const { |
||||
|
getWorkerUrl, |
||||
|
getCouchUrl, |
||||
|
getMinioUrl, |
||||
|
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() |
||||
|
const objClient = new AWS.S3({ |
||||
|
endpoint: await getMinioUrl(), |
||||
|
s3ForcePathStyle: true, // needed with minio?
|
||||
|
signatureVersion: "v4", |
||||
|
params: { |
||||
|
Bucket: verification.bucket, |
||||
|
}, |
||||
|
}) |
||||
|
// no metadata, aws has account ID in metadata
|
||||
|
const metadata = {} |
||||
|
await deployToObjectStore(appId, objClient, metadata) |
||||
|
} |
||||
|
|
||||
|
exports.replicateDb = async function(deployment) { |
||||
|
const appId = deployment.getAppId() |
||||
|
const verification = deployment.getVerification() |
||||
|
return performReplication( |
||||
|
appId, |
||||
|
verification.couchDbSession, |
||||
|
await getCouchUrl() |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
const fs = require("fs") |
||||
|
const sanitize = require("sanitize-s3-objectkey") |
||||
|
const { walkDir } = require("../../../utilities") |
||||
|
const { join } = require("../../../utilities/centralPath") |
||||
|
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
||||
|
const fetch = require("node-fetch") |
||||
|
const PouchDB = require("../../../db") |
||||
|
const CouchDB = require("pouchdb") |
||||
|
|
||||
|
const CONTENT_TYPE_MAP = { |
||||
|
html: "text/html", |
||||
|
css: "text/css", |
||||
|
js: "application/javascript", |
||||
|
} |
||||
|
|
||||
|
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, metadata, client, file }) { |
||||
|
const extension = [...file.name.split(".")].pop() |
||||
|
const fileBytes = fs.readFileSync(file.path) |
||||
|
|
||||
|
const upload = await client |
||||
|
.upload({ |
||||
|
// windows file paths need to be converted to forward slashes for s3
|
||||
|
Key: sanitize(s3Key).replace(/\\/g, "/"), |
||||
|
Body: fileBytes, |
||||
|
ContentType: file.type || CONTENT_TYPE_MAP[extension.toLowerCase()], |
||||
|
Metadata: metadata, |
||||
|
}) |
||||
|
.promise() |
||||
|
|
||||
|
return { |
||||
|
size: file.size, |
||||
|
name: file.name, |
||||
|
extension, |
||||
|
url: upload.Location, |
||||
|
key: upload.Key, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.deployToObjectStore = async function(appId, objectClient, 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({ |
||||
|
file: { |
||||
|
path: filePath, |
||||
|
name: filePathParts.pop(), |
||||
|
}, |
||||
|
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`), |
||||
|
client: objectClient, |
||||
|
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}`, |
||||
|
client: objectClient, |
||||
|
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)) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants") |
||||
|
const { |
||||
|
getHostingInfo, |
||||
|
HostingTypes, |
||||
|
getAppUrl, |
||||
|
} = require("../../utilities/builder/hosting") |
||||
|
|
||||
|
exports.fetchInfo = async ctx => { |
||||
|
ctx.body = { |
||||
|
types: Object.values(HostingTypes), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.save = async ctx => { |
||||
|
const db = new CouchDB(BUILDER_CONFIG_DB) |
||||
|
const { type } = ctx.request.body |
||||
|
if (type === HostingTypes.CLOUD && ctx.request.body._rev) { |
||||
|
ctx.body = await db.remove({ |
||||
|
...ctx.request.body, |
||||
|
_id: HOSTING_DOC, |
||||
|
}) |
||||
|
} else { |
||||
|
ctx.body = await db.put({ |
||||
|
...ctx.request.body, |
||||
|
_id: HOSTING_DOC, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.fetch = async ctx => { |
||||
|
ctx.body = await getHostingInfo() |
||||
|
} |
||||
|
|
||||
|
exports.fetchUrls = async ctx => { |
||||
|
ctx.body = { |
||||
|
app: await getAppUrl(ctx.appId), |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const controller = require("../controllers/hosting") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER } = require("../../utilities/security/permissions") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router |
||||
|
.get("/api/hosting/info", authorized(BUILDER), controller.fetchInfo) |
||||
|
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls) |
||||
|
.get("/api/hosting", authorized(BUILDER), controller.fetch) |
||||
|
.post("/api/hosting", authorized(BUILDER), controller.save) |
||||
|
|
||||
|
module.exports = router |
||||
@ -0,0 +1,7 @@ |
|||||
|
### Self hosting |
||||
|
This directory contains utilities that are needed for self hosted platforms to operate. |
||||
|
These will mostly be utilities, necessary to the operation of the server e.g. storing self |
||||
|
hosting specific options and attributes to CouchDB. |
||||
|
|
||||
|
All the internal operations should be exposed through the `index.js` so importing |
||||
|
the self host directory should give you everything you need. |
||||
@ -0,0 +1,44 @@ |
|||||
|
const CouchDB = require("../db") |
||||
|
const env = require("../environment") |
||||
|
const newid = require("../db/newid") |
||||
|
|
||||
|
const SELF_HOST_DB = "self-host-db" |
||||
|
const SELF_HOST_DOC = "self-host-info" |
||||
|
|
||||
|
async function createSelfHostDB(db) { |
||||
|
await db.put({ |
||||
|
_id: "_design/database", |
||||
|
views: {}, |
||||
|
}) |
||||
|
const selfHostInfo = { |
||||
|
_id: SELF_HOST_DOC, |
||||
|
apiKeyId: newid(), |
||||
|
} |
||||
|
await db.put(selfHostInfo) |
||||
|
return selfHostInfo |
||||
|
} |
||||
|
|
||||
|
exports.init = async () => { |
||||
|
if (!env.SELF_HOSTED) { |
||||
|
return |
||||
|
} |
||||
|
const db = new CouchDB(SELF_HOST_DB) |
||||
|
try { |
||||
|
await db.get(SELF_HOST_DOC) |
||||
|
} catch (err) { |
||||
|
// failed to retrieve
|
||||
|
if (err.status === 404) { |
||||
|
await createSelfHostDB(db) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.getSelfHostInfo = async () => { |
||||
|
const db = new CouchDB(SELF_HOST_DB) |
||||
|
return db.get(SELF_HOST_DOC) |
||||
|
} |
||||
|
|
||||
|
exports.getSelfHostAPIKey = async () => { |
||||
|
const info = await exports.getSelfHostInfo() |
||||
|
return info ? info.apiKeyId : null |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants") |
||||
|
|
||||
|
const PROD_HOSTING_URL = "app.budi.live" |
||||
|
|
||||
|
function getProtocol(hostingInfo) { |
||||
|
return hostingInfo.useHttps ? "https://" : "http://" |
||||
|
} |
||||
|
|
||||
|
async function getURLWithPath(pathIfSelfHosted) { |
||||
|
const hostingInfo = await exports.getHostingInfo() |
||||
|
const protocol = getProtocol(hostingInfo) |
||||
|
const path = |
||||
|
hostingInfo.type === exports.HostingTypes.SELF ? pathIfSelfHosted : "" |
||||
|
return `${protocol}${hostingInfo.hostingUrl}${path}` |
||||
|
} |
||||
|
|
||||
|
exports.HostingTypes = { |
||||
|
CLOUD: "cloud", |
||||
|
SELF: "self", |
||||
|
} |
||||
|
|
||||
|
exports.getHostingInfo = async () => { |
||||
|
const db = new CouchDB(BUILDER_CONFIG_DB) |
||||
|
let doc |
||||
|
try { |
||||
|
doc = await db.get(HOSTING_DOC) |
||||
|
} catch (err) { |
||||
|
// don't write this doc, want to be able to update these default props
|
||||
|
// for our servers with a new release without needing to worry about state of
|
||||
|
// PouchDB in peoples installations
|
||||
|
doc = { |
||||
|
_id: HOSTING_DOC, |
||||
|
type: exports.HostingTypes.CLOUD, |
||||
|
hostingUrl: PROD_HOSTING_URL, |
||||
|
selfHostKey: "", |
||||
|
templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com", |
||||
|
useHttps: true, |
||||
|
} |
||||
|
} |
||||
|
return doc |
||||
|
} |
||||
|
|
||||
|
exports.getAppUrl = async appId => { |
||||
|
const hostingInfo = await exports.getHostingInfo() |
||||
|
const protocol = getProtocol(hostingInfo) |
||||
|
let url |
||||
|
if (hostingInfo.type === exports.HostingTypes.CLOUD) { |
||||
|
url = `${protocol}${appId}.${hostingInfo.hostingUrl}` |
||||
|
} else { |
||||
|
url = `${protocol}${hostingInfo.hostingUrl}/app` |
||||
|
} |
||||
|
return url |
||||
|
} |
||||
|
|
||||
|
exports.getWorkerUrl = async () => { |
||||
|
return getURLWithPath("/worker") |
||||
|
} |
||||
|
|
||||
|
exports.getMinioUrl = async () => { |
||||
|
return getURLWithPath("/") |
||||
|
} |
||||
|
|
||||
|
exports.getCouchUrl = async () => { |
||||
|
return getURLWithPath("/db") |
||||
|
} |
||||
|
|
||||
|
exports.getSelfHostKey = async () => { |
||||
|
const hostingInfo = await exports.getHostingInfo() |
||||
|
return hostingInfo.selfHostKey |
||||
|
} |
||||
|
|
||||
|
exports.getTemplatesUrl = async (appId, type, name) => { |
||||
|
const hostingInfo = await exports.getHostingInfo() |
||||
|
const protocol = getProtocol(hostingInfo) |
||||
|
let path |
||||
|
if (type && name) { |
||||
|
path = `templates/type/${name}.tar.gz` |
||||
|
} else { |
||||
|
path = "manifest.json" |
||||
|
} |
||||
|
return `${protocol}${hostingInfo.templatesUrl}/${path}` |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
const { apiKeyTable } = require("../../db/dynamoClient") |
||||
|
const env = require("../../environment") |
||||
|
const { getSelfHostAPIKey } = require("../../selfhost") |
||||
|
|
||||
|
/** |
||||
|
* This file purely exists so that we can centralise all logic pertaining to API keys, as their usage differs |
||||
|
* in our Cloud environment versus self hosted. |
||||
|
*/ |
||||
|
|
||||
|
exports.isAPIKeyValid = async apiKeyId => { |
||||
|
if (env.CLOUD && !env.SELF_HOSTED) { |
||||
|
let apiKeyInfo = await apiKeyTable.get({ |
||||
|
primary: apiKeyId, |
||||
|
}) |
||||
|
return apiKeyInfo != null |
||||
|
} |
||||
|
if (env.SELF_HOSTED) { |
||||
|
const selfHostKey = await getSelfHostAPIKey() |
||||
|
// if the api key supplied is correct then return structure similar
|
||||
|
return apiKeyId === selfHostKey ? { pk: apiKeyId } : null |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,2 @@ |
|||||
|
node_modules/ |
||||
|
.env |
||||
@ -0,0 +1,15 @@ |
|||||
|
FROM node:12-alpine |
||||
|
|
||||
|
WORKDIR /app |
||||
|
|
||||
|
# copy files and install dependencies |
||||
|
COPY . ./ |
||||
|
RUN yarn |
||||
|
|
||||
|
EXPOSE 4001 |
||||
|
|
||||
|
# have to add node environment production after install |
||||
|
# due to this causing yarn to stop installing dev dependencies |
||||
|
# which are actually needed to get this environment up and running |
||||
|
ENV NODE_ENV=production |
||||
|
CMD ["yarn", "run:docker"] |
||||
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"name": "@budibase/deployment", |
||||
|
"email": "hi@budibase.com", |
||||
|
"version": "0.3.8", |
||||
|
"description": "Budibase Deployment Server", |
||||
|
"main": "src/index.js", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "https://github.com/Budibase/budibase.git" |
||||
|
}, |
||||
|
"keywords": [ |
||||
|
"budibase" |
||||
|
], |
||||
|
"scripts": { |
||||
|
"run:docker": "node src/index.js" |
||||
|
}, |
||||
|
"author": "Budibase", |
||||
|
"license": "AGPL-3.0-or-later", |
||||
|
"dependencies": { |
||||
|
"@koa/router": "^8.0.0", |
||||
|
"aws-sdk": "^2.811.0", |
||||
|
"got": "^11.8.1", |
||||
|
"joi": "^17.2.1", |
||||
|
"koa": "^2.7.0", |
||||
|
"koa-body": "^4.2.0", |
||||
|
"koa-compress": "^4.0.1", |
||||
|
"koa-pino-logger": "^3.0.0", |
||||
|
"koa-send": "^5.0.0", |
||||
|
"koa-session": "^5.12.0", |
||||
|
"koa-static": "^5.0.0", |
||||
|
"pino-pretty": "^4.0.0", |
||||
|
"server-destroy": "^1.0.1" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
const env = require("../../environment") |
||||
|
const got = require("got") |
||||
|
const AWS = require("aws-sdk") |
||||
|
|
||||
|
const APP_BUCKET = "app-assets" |
||||
|
// this doesn't matter in self host
|
||||
|
const REGION = "eu-west-1" |
||||
|
const PUBLIC_READ_POLICY = { |
||||
|
Version: "2012-10-17", |
||||
|
Statement: [ |
||||
|
{ |
||||
|
Effect: "Allow", |
||||
|
Principal: { |
||||
|
AWS: ["*"], |
||||
|
}, |
||||
|
Action: "s3:GetObject", |
||||
|
Resource: [`arn:aws:s3:::${APP_BUCKET}/*`], |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
|
||||
|
async function getCouchSession() { |
||||
|
// fetch session token for the api user
|
||||
|
const session = await got.post(`${env.RAW_COUCH_DB_URL}/_session`, { |
||||
|
responseType: "json", |
||||
|
credentials: "include", |
||||
|
json: { |
||||
|
username: env.COUCH_DB_USERNAME, |
||||
|
password: env.COUCH_DB_PASSWORD, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
const cookie = session.headers["set-cookie"][0] |
||||
|
// Get the session cookie value only
|
||||
|
return cookie.split(";")[0] |
||||
|
} |
||||
|
|
||||
|
async function getMinioSession() { |
||||
|
AWS.config.update({ |
||||
|
accessKeyId: env.MINIO_ACCESS_KEY, |
||||
|
secretAccessKey: env.MINIO_SECRET_KEY, |
||||
|
}) |
||||
|
|
||||
|
// make sure the bucket exists
|
||||
|
const objClient = new AWS.S3({ |
||||
|
endpoint: env.RAW_MINIO_URL, |
||||
|
region: REGION, |
||||
|
s3ForcePathStyle: true, // needed with minio?
|
||||
|
params: { |
||||
|
Bucket: APP_BUCKET, |
||||
|
}, |
||||
|
}) |
||||
|
// make sure the bucket exists
|
||||
|
try { |
||||
|
await objClient |
||||
|
.headBucket({ |
||||
|
Bucket: APP_BUCKET, |
||||
|
}) |
||||
|
.promise() |
||||
|
} catch (err) { |
||||
|
// bucket doesn't exist create it
|
||||
|
if (err.statusCode === 404) { |
||||
|
await objClient |
||||
|
.createBucket({ |
||||
|
Bucket: APP_BUCKET, |
||||
|
}) |
||||
|
.promise() |
||||
|
} else { |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
// always make sure policy is correct
|
||||
|
await objClient |
||||
|
.putBucketPolicy({ |
||||
|
Bucket: APP_BUCKET, |
||||
|
Policy: JSON.stringify(PUBLIC_READ_POLICY), |
||||
|
}) |
||||
|
.promise() |
||||
|
// Ideally want to send back some pre-signed URLs for files that are to be uploaded
|
||||
|
return { |
||||
|
accessKeyId: env.MINIO_ACCESS_KEY, |
||||
|
secretAccessKey: env.MINIO_SECRET_KEY, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.deploy = async ctx => { |
||||
|
ctx.body = { |
||||
|
couchDbSession: await getCouchSession(), |
||||
|
bucket: APP_BUCKET, |
||||
|
objectStoreSession: await getMinioSession(), |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const compress = require("koa-compress") |
||||
|
const zlib = require("zlib") |
||||
|
const { routes } = require("./routes") |
||||
|
|
||||
|
const router = new Router() |
||||
|
|
||||
|
router |
||||
|
.use( |
||||
|
compress({ |
||||
|
threshold: 2048, |
||||
|
gzip: { |
||||
|
flush: zlib.Z_SYNC_FLUSH, |
||||
|
}, |
||||
|
deflate: { |
||||
|
flush: zlib.Z_SYNC_FLUSH, |
||||
|
}, |
||||
|
br: false, |
||||
|
}) |
||||
|
) |
||||
|
.use("/health", ctx => (ctx.status = 200)) |
||||
|
|
||||
|
// error handling middleware
|
||||
|
router.use(async (ctx, next) => { |
||||
|
try { |
||||
|
await next() |
||||
|
} catch (err) { |
||||
|
ctx.log.error(err) |
||||
|
ctx.status = err.status || err.statusCode || 500 |
||||
|
ctx.body = { |
||||
|
message: err.message, |
||||
|
status: ctx.status, |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
router.get("/health", ctx => (ctx.status = 200)) |
||||
|
|
||||
|
// authenticated routes
|
||||
|
for (let route of routes) { |
||||
|
router.use(route.routes()) |
||||
|
router.use(route.allowedMethods()) |
||||
|
} |
||||
|
|
||||
|
module.exports = router |
||||
@ -0,0 +1,9 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const controller = require("../controllers/deploy") |
||||
|
const checkKey = require("../../middleware/check-key") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router.post("/api/deploy", checkKey, controller.deploy) |
||||
|
|
||||
|
module.exports = router |
||||
@ -0,0 +1,3 @@ |
|||||
|
const deployRoutes = require("./deploy") |
||||
|
|
||||
|
exports.routes = [deployRoutes] |
||||
@ -0,0 +1,18 @@ |
|||||
|
module.exports = { |
||||
|
SELF_HOSTED: process.env.SELF_HOSTED, |
||||
|
WORKER_API_KEY: process.env.WORKER_API_KEY, |
||||
|
PORT: process.env.PORT, |
||||
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, |
||||
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, |
||||
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME, |
||||
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, |
||||
|
RAW_COUCH_DB_URL: process.env.RAW_COUCH_DB_URL, |
||||
|
RAW_MINIO_URL: process.env.RAW_MINIO_URL, |
||||
|
COUCH_DB_PORT: process.env.COUCH_DB_PORT, |
||||
|
MINIO_PORT: process.env.MINIO_PORT, |
||||
|
SELF_HOST_KEY: process.env.SELF_HOST_KEY, |
||||
|
_set(key, value) { |
||||
|
process.env[key] = value |
||||
|
module.exports[key] = value |
||||
|
}, |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
const Koa = require("koa") |
||||
|
const destroyable = require("server-destroy") |
||||
|
const koaBody = require("koa-body") |
||||
|
const logger = require("koa-pino-logger") |
||||
|
const http = require("http") |
||||
|
const api = require("./api") |
||||
|
const env = require("./environment") |
||||
|
|
||||
|
const app = new Koa() |
||||
|
|
||||
|
if (!env.SELF_HOSTED) { |
||||
|
throw "Currently this service only supports use in self hosting" |
||||
|
} |
||||
|
|
||||
|
// set up top level koa middleware
|
||||
|
app.use(koaBody({ multipart: true })) |
||||
|
|
||||
|
app.use( |
||||
|
logger({ |
||||
|
prettyPrint: { |
||||
|
levelFirst: true, |
||||
|
}, |
||||
|
level: env.LOG_LEVEL || "error", |
||||
|
}) |
||||
|
) |
||||
|
|
||||
|
// api routes
|
||||
|
app.use(api.routes()) |
||||
|
|
||||
|
const server = http.createServer(app.callback()) |
||||
|
destroyable(server) |
||||
|
|
||||
|
server.on("close", () => console.log("Server Closed")) |
||||
|
|
||||
|
module.exports = server.listen(env.PORT || 4002, async () => { |
||||
|
console.log(`Worker running on ${JSON.stringify(server.address())}`) |
||||
|
}) |
||||
|
|
||||
|
process.on("uncaughtException", err => { |
||||
|
console.error(err) |
||||
|
server.close() |
||||
|
server.destroy() |
||||
|
}) |
||||
|
|
||||
|
process.on("SIGTERM", () => { |
||||
|
server.close() |
||||
|
server.destroy() |
||||
|
}) |
||||
@ -0,0 +1,12 @@ |
|||||
|
const env = require("../environment") |
||||
|
|
||||
|
module.exports = async (ctx, next) => { |
||||
|
if ( |
||||
|
!ctx.request.body.selfHostKey || |
||||
|
env.SELF_HOST_KEY !== ctx.request.body.selfHostKey |
||||
|
) { |
||||
|
ctx.throw(401, "Deployment unauthorised") |
||||
|
} else { |
||||
|
await next() |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue