forked from tsai/budibase
154 changed files with 2294 additions and 2561 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> |
||||
@ -0,0 +1,20 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
export default function (url) { |
||||
|
const store = writable({ status: "LOADING", data: {}, error: {} }) |
||||
|
|
||||
|
async function get() { |
||||
|
store.update(u => ({ ...u, status: "LOADING" })) |
||||
|
try { |
||||
|
const response = await api.get(url) |
||||
|
store.set({ data: await response.json(), status: "SUCCESS" }) |
||||
|
} catch (e) { |
||||
|
store.set({ data: {}, error: e, status: "ERROR" }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get() |
||||
|
|
||||
|
return { subscribe: store.subscribe, refresh: get } |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
export { default as fetchData } from "./fetchData" |
||||
|
export { |
||||
|
buildStyle, |
||||
|
convertCamel, |
||||
|
pipe, |
||||
|
capitalise, |
||||
|
get_name, |
||||
|
get_capitalised_name, |
||||
|
} from "./helpers" |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { emailValidator, requiredValidator } from "./validators" |
||||
|
export { createValidationStore } from "./validation" |
||||
@ -0,0 +1,23 @@ |
|||||
|
import { writable, derived } from "svelte/store" |
||||
|
|
||||
|
export function createValidationStore(initialValue, ...validators) { |
||||
|
let touched = false |
||||
|
|
||||
|
const value = writable(initialValue || "") |
||||
|
const error = derived(value, $v => validate($v, validators)) |
||||
|
const touchedStore = derived(value, () => { |
||||
|
if (!touched) { |
||||
|
touched = true |
||||
|
return false |
||||
|
} |
||||
|
return touched |
||||
|
}) |
||||
|
|
||||
|
return [value, error, touchedStore] |
||||
|
} |
||||
|
|
||||
|
function validate(value, validators) { |
||||
|
const failing = validators.find(v => v(value) !== true) |
||||
|
|
||||
|
return failing && failing(value) |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
export function emailValidator(value) { |
||||
|
return ( |
||||
|
(value && |
||||
|
!!value.match( |
||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ |
||||
|
)) || |
||||
|
"Please enter a valid email" |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export function requiredValidator(value) { |
||||
|
return ( |
||||
|
(value !== undefined && value !== null && value !== "") || |
||||
|
"This field is required" |
||||
|
) |
||||
|
} |
||||
@ -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> |
|
||||
@ -0,0 +1,97 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Layout, |
||||
|
Heading, |
||||
|
Body, |
||||
|
Input, |
||||
|
Button, |
||||
|
notifications, |
||||
|
} from "@budibase/bbui" |
||||
|
import { goto, params } from "@roxi/routify" |
||||
|
import { createValidationStore, requiredValidator } from "helpers/validation" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
const [password, passwordError, passwordTouched] = createValidationStore( |
||||
|
"", |
||||
|
requiredValidator |
||||
|
) |
||||
|
const [repeat, _, repeatTouched] = createValidationStore( |
||||
|
"", |
||||
|
requiredValidator |
||||
|
) |
||||
|
const inviteCode = $params["?code"] |
||||
|
|
||||
|
async function acceptInvite() { |
||||
|
try { |
||||
|
const res = await users.acceptInvite(inviteCode, $password) |
||||
|
if (!res) { |
||||
|
throw new Error(res.message) |
||||
|
} |
||||
|
notifications.success(`User created.`) |
||||
|
$goto("../auth/login") |
||||
|
} catch (err) { |
||||
|
notifications.error(err) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<div class="container"> |
||||
|
<Layout gap="XS"> |
||||
|
<img src="https://i.imgur.com/ZKyklgF.png" /> |
||||
|
</Layout> |
||||
|
<div class="center"> |
||||
|
<Layout gap="XS"> |
||||
|
<Heading size="M">Accept Invitation</Heading> |
||||
|
<Body size="M">Please enter a password to setup your user.</Body> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
<Layout gap="XS"> |
||||
|
<Input |
||||
|
label="Password" |
||||
|
type="password" |
||||
|
error={$passwordTouched && $passwordError} |
||||
|
bind:value={$password} |
||||
|
/> |
||||
|
<Input |
||||
|
label="Repeat Password" |
||||
|
type="password" |
||||
|
error={$repeatTouched && |
||||
|
$password !== $repeat && |
||||
|
"Passwords must match"} |
||||
|
bind:value={$repeat} |
||||
|
/> |
||||
|
</Layout> |
||||
|
<Layout gap="S"> |
||||
|
<Button |
||||
|
disabled={!$passwordTouched || !$repeatTouched || $password !== $repeat} |
||||
|
cta |
||||
|
on:click={acceptInvite}>Accept invite</Button |
||||
|
> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
height: 100%; |
||||
|
} |
||||
|
.container { |
||||
|
margin: 0 auto; |
||||
|
width: 260px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
.center { |
||||
|
text-align: center; |
||||
|
} |
||||
|
img { |
||||
|
width: 40px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<script> |
||||
|
import { Page } from "@budibase/bbui" |
||||
|
</script> |
||||
|
|
||||
|
<Page> |
||||
|
<slot /> |
||||
|
</Page> |
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,114 @@ |
|||||
|
<script> |
||||
|
import GoogleLogo from "./_logos/Google.svelte" |
||||
|
import { |
||||
|
Button, |
||||
|
Heading, |
||||
|
Divider, |
||||
|
Page, |
||||
|
Label, |
||||
|
notifications, |
||||
|
Layout, |
||||
|
Input, |
||||
|
Body, |
||||
|
} from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
const ConfigTypes = { |
||||
|
Google: "google", |
||||
|
// Github: "github", |
||||
|
// AzureAD: "ad", |
||||
|
} |
||||
|
|
||||
|
const ConfigFields = { |
||||
|
Google: ["clientID", "clientSecret", "callbackURL"], |
||||
|
} |
||||
|
|
||||
|
let google |
||||
|
|
||||
|
async function save(doc) { |
||||
|
try { |
||||
|
// Save an oauth config |
||||
|
const response = await api.post(`/api/admin/configs`, doc) |
||||
|
const json = await response.json() |
||||
|
if (response.status !== 200) throw new Error(json.message) |
||||
|
google._rev = json._rev |
||||
|
google._id = json._id |
||||
|
|
||||
|
notifications.success(`Settings saved.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Failed to update OAuth settings. ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
// fetch the configs for oauth |
||||
|
const googleResponse = await api.get( |
||||
|
`/api/admin/configs/${ConfigTypes.Google}` |
||||
|
) |
||||
|
const googleDoc = await googleResponse.json() |
||||
|
|
||||
|
if (!googleDoc._id) { |
||||
|
google = { |
||||
|
type: ConfigTypes.Google, |
||||
|
config: {}, |
||||
|
} |
||||
|
} else { |
||||
|
google = googleDoc |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Page> |
||||
|
<Layout noPadding> |
||||
|
<div> |
||||
|
<Heading size="M">OAuth</Heading> |
||||
|
<Body> |
||||
|
Every budibase app comes with basic authentication (email/password) |
||||
|
included. You can add additional authentication methods from the options |
||||
|
below. |
||||
|
</Body> |
||||
|
</div> |
||||
|
<Divider /> |
||||
|
{#if google} |
||||
|
<div> |
||||
|
<Heading size="S"> |
||||
|
<span> |
||||
|
<GoogleLogo /> |
||||
|
Google |
||||
|
</span> |
||||
|
</Heading> |
||||
|
<Body> |
||||
|
To allow users to authenticate using their Google accounts, fill out |
||||
|
the fields below. |
||||
|
</Body> |
||||
|
</div> |
||||
|
|
||||
|
{#each ConfigFields.Google as field} |
||||
|
<div class="form-row"> |
||||
|
<Label size="L">{field}</Label> |
||||
|
<Input bind:value={google.config[field]} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
<div> |
||||
|
<Button primary on:click={() => save(google)}>Save</Button> |
||||
|
</div> |
||||
|
<Divider /> |
||||
|
{/if} |
||||
|
</Layout> |
||||
|
</Page> |
||||
|
|
||||
|
<style> |
||||
|
.form-row { |
||||
|
display: grid; |
||||
|
grid-template-columns: 20% 1fr; |
||||
|
grid-gap: var(--spacing-l); |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-s); |
||||
|
} |
||||
|
</style> |
||||
@ -1,32 +1,19 @@ |
|||||
<script> |
<script> |
||||
import { |
import { |
||||
Menu, |
|
||||
MenuItem, |
|
||||
Button, |
Button, |
||||
Detail, |
Detail, |
||||
Heading, |
Heading, |
||||
Divider, |
|
||||
Label, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
notifications, |
notifications, |
||||
Layout, |
|
||||
Icon, |
Icon, |
||||
Body, |
|
||||
Page, |
Page, |
||||
Select, |
|
||||
Tabs, |
Tabs, |
||||
Tab, |
Tab, |
||||
MenuSection, |
|
||||
MenuSeparator, |
|
||||
} from "@budibase/bbui" |
} from "@budibase/bbui" |
||||
import { goto } from "@roxi/routify" |
import { goto } from "@roxi/routify" |
||||
import { onMount } from "svelte" |
|
||||
import { fade } from "svelte/transition" |
import { fade } from "svelte/transition" |
||||
import { email } from "stores/portal" |
import { email } from "stores/portal" |
||||
import Editor from "components/integration/QueryEditor.svelte" |
import Editor from "components/integration/QueryEditor.svelte" |
||||
import TemplateBindings from "./TemplateBindings.svelte" |
import TemplateBindings from "./_components/TemplateBindings.svelte" |
||||
import api from "builderStore/api" |
|
||||
|
|
||||
const ConfigTypes = { |
const ConfigTypes = { |
||||
SMTP: "smtp", |
SMTP: "smtp", |
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,115 @@ |
|||||
|
<script> |
||||
|
import GoogleLogo from "./_logos/Google.svelte" |
||||
|
import { |
||||
|
Button, |
||||
|
Heading, |
||||
|
Divider, |
||||
|
Label, |
||||
|
notifications, |
||||
|
Layout, |
||||
|
Input, |
||||
|
Body, |
||||
|
Page, |
||||
|
} from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
const ConfigTypes = { |
||||
|
Google: "google", |
||||
|
// Github: "github", |
||||
|
// AzureAD: "ad", |
||||
|
} |
||||
|
|
||||
|
const ConfigFields = { |
||||
|
Google: ["clientID", "clientSecret", "callbackURL"], |
||||
|
} |
||||
|
|
||||
|
let google |
||||
|
|
||||
|
async function save(doc) { |
||||
|
try { |
||||
|
// Save an oauth config |
||||
|
const response = await api.post(`/api/admin/configs`, doc) |
||||
|
const json = await response.json() |
||||
|
if (response.status !== 200) throw new Error(json.message) |
||||
|
google._rev = json._rev |
||||
|
google._id = json._id |
||||
|
|
||||
|
notifications.success(`Settings saved.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Failed to update OAuth settings. ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
// fetch the configs for oauth |
||||
|
const googleResponse = await api.get( |
||||
|
`/api/admin/configs/${ConfigTypes.Google}` |
||||
|
) |
||||
|
const googleDoc = await googleResponse.json() |
||||
|
|
||||
|
if (!googleDoc._id) { |
||||
|
google = { |
||||
|
type: ConfigTypes.Google, |
||||
|
config: {}, |
||||
|
} |
||||
|
} else { |
||||
|
google = googleDoc |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Page> |
||||
|
<header> |
||||
|
<Heading size="M">OAuth</Heading> |
||||
|
<Body size="S"> |
||||
|
Every budibase app comes with basic authentication (email/password) |
||||
|
included. You can add additional authentication methods from the options |
||||
|
below. |
||||
|
</Body> |
||||
|
</header> |
||||
|
<Divider /> |
||||
|
{#if google} |
||||
|
<div class="config-form"> |
||||
|
<Layout gap="S"> |
||||
|
<Heading size="S"> |
||||
|
<span> |
||||
|
<GoogleLogo /> |
||||
|
Google |
||||
|
</span> |
||||
|
</Heading> |
||||
|
{#each ConfigFields.Google as field} |
||||
|
<div class="form-row"> |
||||
|
<Label>{field}</Label> |
||||
|
<Input bind:value={google.config[field]} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</Layout> |
||||
|
<Button primary on:click={() => save(google)}>Save</Button> |
||||
|
</div> |
||||
|
<Divider /> |
||||
|
{/if} |
||||
|
</Page> |
||||
|
|
||||
|
<style> |
||||
|
.config-form { |
||||
|
margin-top: 42px; |
||||
|
margin-bottom: 42px; |
||||
|
} |
||||
|
|
||||
|
.form-row { |
||||
|
display: grid; |
||||
|
grid-template-columns: 20% 1fr; |
||||
|
grid-gap: var(--spacing-l); |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-s); |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
margin-bottom: 42px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,168 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import { |
||||
|
ActionButton, |
||||
|
Button, |
||||
|
Layout, |
||||
|
Heading, |
||||
|
Body, |
||||
|
Divider, |
||||
|
Label, |
||||
|
Input, |
||||
|
Modal, |
||||
|
Table, |
||||
|
ModalContent, |
||||
|
notifications, |
||||
|
} from "@budibase/bbui" |
||||
|
import { fetchData } from "helpers" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte" |
||||
|
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" |
||||
|
|
||||
|
export let userId |
||||
|
let deleteUserModal |
||||
|
let editRolesModal |
||||
|
|
||||
|
const roleSchema = { |
||||
|
name: { displayName: "App" }, |
||||
|
role: {}, |
||||
|
} |
||||
|
|
||||
|
// Merge the Apps list and the roles response to get something that makes sense for the table |
||||
|
$: appList = Object.keys($apps?.data).map(id => ({ |
||||
|
...$apps?.data?.[id], |
||||
|
_id: id, |
||||
|
role: [$roleFetch?.data?.roles?.[id]], |
||||
|
})) |
||||
|
let selectedApp |
||||
|
|
||||
|
const roleFetch = fetchData(`/api/admin/users/${userId}`) |
||||
|
const apps = fetchData(`/api/admin/roles`) |
||||
|
|
||||
|
async function deleteUser() { |
||||
|
const res = await users.del(userId) |
||||
|
if (res.message) { |
||||
|
notifications.success(`User ${$roleFetch?.data?.email} deleted.`) |
||||
|
$goto("./") |
||||
|
} else { |
||||
|
notifications.error("Failed to delete user.") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function openUpdateRolesModal({ detail }) { |
||||
|
console.log(detail) |
||||
|
selectedApp = detail |
||||
|
editRolesModal.show() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Layout noPadding gap="XS"> |
||||
|
<div class="back"> |
||||
|
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid" |
||||
|
>Back to users</ActionButton |
||||
|
> |
||||
|
</div> |
||||
|
<div class="heading"> |
||||
|
<Layout noPadding gap="XS"> |
||||
|
<Heading>User: {$roleFetch?.data?.email}</Heading> |
||||
|
<Body |
||||
|
>Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro |
||||
|
ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit |
||||
|
veritatis quos quas laborum magnam tenetur perspiciatis ex hic. |
||||
|
</Body> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
<Divider size="S" /> |
||||
|
<div class="general"> |
||||
|
<Heading size="S">General</Heading> |
||||
|
<div class="fields"> |
||||
|
<div class="field"> |
||||
|
<Label size="L">Email</Label> |
||||
|
<Input disabled thin value={$roleFetch?.data?.email} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="regenerate"> |
||||
|
<ActionButton size="S" icon="Refresh" quiet |
||||
|
>Regenerate password</ActionButton |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
<Divider size="S" /> |
||||
|
<div class="roles"> |
||||
|
<Heading size="S">Configure roles</Heading> |
||||
|
<Table |
||||
|
on:click={openUpdateRolesModal} |
||||
|
schema={roleSchema} |
||||
|
data={appList} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
customRenderers={[{ column: "role", component: TagsRenderer }]} |
||||
|
/> |
||||
|
</div> |
||||
|
<Divider size="S" /> |
||||
|
<div class="delete"> |
||||
|
<Layout gap="S" noPadding |
||||
|
><Heading size="S">Delete user</Heading> |
||||
|
<Body>Deleting a user completely removes them from your account.</Body> |
||||
|
<div class="delete-button"> |
||||
|
<Button warning on:click={deleteUserModal.show}>Delete user</Button> |
||||
|
</div></Layout |
||||
|
> |
||||
|
</div> |
||||
|
</Layout> |
||||
|
|
||||
|
<Modal bind:this={deleteUserModal}> |
||||
|
<ModalContent |
||||
|
warning |
||||
|
onConfirm={deleteUser} |
||||
|
title="Delete User" |
||||
|
confirmText="Delete user" |
||||
|
cancelText="Cancel" |
||||
|
showCloseIcon={false} |
||||
|
> |
||||
|
<Body |
||||
|
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong |
||||
|
></Body |
||||
|
> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
<Modal bind:this={editRolesModal}> |
||||
|
<UpdateRolesModal |
||||
|
app={selectedApp} |
||||
|
user={$roleFetch.data} |
||||
|
on:update={roleFetch.refresh} |
||||
|
/> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
.fields { |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-m); |
||||
|
margin-top: var(--spacing-xl); |
||||
|
} |
||||
|
.field { |
||||
|
display: grid; |
||||
|
grid-template-columns: 32% 1fr; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.heading { |
||||
|
margin-bottom: var(--spacing-xl); |
||||
|
} |
||||
|
.general { |
||||
|
position: relative; |
||||
|
margin: var(--spacing-xl) 0; |
||||
|
} |
||||
|
.roles { |
||||
|
margin: var(--spacing-xl) 0; |
||||
|
} |
||||
|
.delete { |
||||
|
margin-top: var(--spacing-xl); |
||||
|
} |
||||
|
.regenerate { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,59 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Body, |
||||
|
Input, |
||||
|
Select, |
||||
|
ModalContent, |
||||
|
notifications, |
||||
|
} from "@budibase/bbui" |
||||
|
import { createValidationStore, emailValidator } from "helpers/validation" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
export let disabled |
||||
|
|
||||
|
const options = ["Email onboarding", "Basic onboarding"] |
||||
|
let selected = options[0] |
||||
|
|
||||
|
const [email, error, touched] = createValidationStore("", emailValidator) |
||||
|
|
||||
|
async function createUserFlow() { |
||||
|
const res = await users.invite($email) |
||||
|
console.log(res) |
||||
|
if (res.status) { |
||||
|
notifications.error(res.message) |
||||
|
} else { |
||||
|
notifications.success(res.message) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
onConfirm={createUserFlow} |
||||
|
size="M" |
||||
|
title="Add new user options" |
||||
|
confirmText="Add user" |
||||
|
confirmDisabled={disabled} |
||||
|
cancelText="Cancel" |
||||
|
disabled={$error} |
||||
|
showCloseIcon={false} |
||||
|
> |
||||
|
<Body noPadding |
||||
|
>If you have SMTP configured and an email for the new user, you can use the |
||||
|
automated email onboarding flow. Otherwise, use our basic onboarding process |
||||
|
with autogenerated passwords.</Body |
||||
|
> |
||||
|
<Select |
||||
|
placeholder={null} |
||||
|
bind:value={selected} |
||||
|
on:change |
||||
|
{options} |
||||
|
label="Add new user via:" |
||||
|
/> |
||||
|
<Input |
||||
|
type="email" |
||||
|
bind:value={$email} |
||||
|
error={$touched && $error} |
||||
|
placeholder="john@doe.com" |
||||
|
label="Email" |
||||
|
/> |
||||
|
</ModalContent> |
||||
@ -0,0 +1,40 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Body, Input, notifications } from "@budibase/bbui" |
||||
|
import { createValidationStore, emailValidator } from "helpers/validation" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
const [email, error, touched] = createValidationStore("", emailValidator) |
||||
|
const password = Math.random().toString(36).substr(2, 20) |
||||
|
|
||||
|
async function createUser() { |
||||
|
const res = await users.create({ email: $email, password }) |
||||
|
if (res.status) { |
||||
|
notifications.error(res.message) |
||||
|
} else { |
||||
|
notifications.success("Succesfully created user") |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
onConfirm={createUser} |
||||
|
size="M" |
||||
|
title="Basic user onboarding" |
||||
|
confirmText="Continue" |
||||
|
cancelText="Cancel" |
||||
|
disabled={$error} |
||||
|
error={$touched && $error} |
||||
|
showCloseIcon={false} |
||||
|
> |
||||
|
<Body noPadding |
||||
|
>Below you will find the user’s username and password. The password will not |
||||
|
be accessible from this point. Please download the credentials.</Body |
||||
|
> |
||||
|
<Input |
||||
|
type="email" |
||||
|
label="Username" |
||||
|
bind:value={$email} |
||||
|
error={$touched && $error} |
||||
|
/> |
||||
|
<Input disabled label="Password" value={password} /> |
||||
|
</ModalContent> |
||||
@ -0,0 +1,20 @@ |
|||||
|
<script> |
||||
|
import { Tag, Tags } from "@budibase/bbui" |
||||
|
export let value |
||||
|
|
||||
|
const displayLimit = 5 |
||||
|
|
||||
|
$: tags = value?.slice(0, displayLimit) ?? [] |
||||
|
$: leftover = (value?.length ?? 0) - tags.length |
||||
|
</script> |
||||
|
|
||||
|
<Tags> |
||||
|
{#each tags as tag} |
||||
|
<Tag> |
||||
|
{tag} |
||||
|
</Tag> |
||||
|
{/each} |
||||
|
{#if leftover} |
||||
|
<Tag>+{leftover} more</Tag> |
||||
|
{/if} |
||||
|
</Tags> |
||||
@ -0,0 +1,51 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
import { Body, Select, ModalContent, notifications } from "@budibase/bbui" |
||||
|
import { fetchData } from "helpers" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
export let app |
||||
|
export let user |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
const roles = app.roles |
||||
|
let options = roles.map(role => role._id) |
||||
|
let selectedRole |
||||
|
|
||||
|
async function updateUserRoles() { |
||||
|
const res = await users.updateRoles({ |
||||
|
...user, |
||||
|
roles: { |
||||
|
...user.roles, |
||||
|
[app._id]: selectedRole, |
||||
|
}, |
||||
|
}) |
||||
|
if (res.status === 400) { |
||||
|
notifications.error("Failed to update role") |
||||
|
} else { |
||||
|
notifications.success("Roles updated") |
||||
|
dispatch("update") |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
onConfirm={updateUserRoles} |
||||
|
title="Update App Roles" |
||||
|
confirmText="Update roles" |
||||
|
cancelText="Cancel" |
||||
|
size="M" |
||||
|
showCloseIcon={false} |
||||
|
> |
||||
|
<Body noPadding |
||||
|
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body |
||||
|
> |
||||
|
<Select |
||||
|
placeholder={null} |
||||
|
bind:value={selectedRole} |
||||
|
on:change |
||||
|
{options} |
||||
|
label="Select roles:" |
||||
|
/> |
||||
|
</ModalContent> |
||||
@ -0,0 +1,105 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import { |
||||
|
Heading, |
||||
|
Body, |
||||
|
Divider, |
||||
|
Button, |
||||
|
ButtonGroup, |
||||
|
Search, |
||||
|
Table, |
||||
|
Label, |
||||
|
Layout, |
||||
|
Modal, |
||||
|
} from "@budibase/bbui" |
||||
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte" |
||||
|
import AddUserModal from "./_components/AddUserModal.svelte" |
||||
|
import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte" |
||||
|
import { users } from "stores/portal" |
||||
|
|
||||
|
users.init() |
||||
|
|
||||
|
const schema = { |
||||
|
email: {}, |
||||
|
status: { displayName: "Development Access", type: "boolean" }, |
||||
|
// role: { type: "options" }, |
||||
|
group: {}, |
||||
|
// access: {}, |
||||
|
// group: {} |
||||
|
} |
||||
|
|
||||
|
let search |
||||
|
let email |
||||
|
$: filteredUsers = $users |
||||
|
.filter(user => user.email.includes(search || "")) |
||||
|
.map(user => ({ ...user, group: ["All"] })) |
||||
|
|
||||
|
let createUserModal |
||||
|
let basicOnboardingModal |
||||
|
|
||||
|
function openBasicOnoboardingModal() { |
||||
|
createUserModal.hide() |
||||
|
basicOnboardingModal.show() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Layout> |
||||
|
<div class="heading"> |
||||
|
<Heading>Users</Heading> |
||||
|
<Body |
||||
|
>Users are the common denominator in Budibase. Each user is assigned to a |
||||
|
group that contains apps and permissions. In this section, you can add |
||||
|
users, or edit and delete an existing user.</Body |
||||
|
> |
||||
|
</div> |
||||
|
<Divider size="S" /> |
||||
|
|
||||
|
<div class="users"> |
||||
|
<Heading size="S">Users</Heading> |
||||
|
<div class="field"> |
||||
|
<Label size="L">Search / filter</Label> |
||||
|
<Search bind:value={search} placeholder="" /> |
||||
|
</div> |
||||
|
<div class="buttons"> |
||||
|
<ButtonGroup> |
||||
|
<Button disabled secondary>Import users</Button> |
||||
|
<Button overBackground on:click={createUserModal.show}>Add user</Button> |
||||
|
</ButtonGroup> |
||||
|
</div> |
||||
|
<Table |
||||
|
on:click={({ detail }) => $goto(`./${detail._id}`)} |
||||
|
{schema} |
||||
|
data={filteredUsers || $users} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
customRenderers={[{ column: "group", component: TagsRenderer }]} |
||||
|
/> |
||||
|
</div> |
||||
|
</Layout> |
||||
|
|
||||
|
<Modal bind:this={createUserModal} |
||||
|
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal |
||||
|
> |
||||
|
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal> |
||||
|
|
||||
|
<style> |
||||
|
.users { |
||||
|
position: relative; |
||||
|
} |
||||
|
.field { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
flex-direction: row; |
||||
|
grid-gap: var(--spacing-m); |
||||
|
margin: var(--spacing-xl) 0; |
||||
|
} |
||||
|
.field > :global(*) + :global(*) { |
||||
|
margin-left: var(--spacing-m); |
||||
|
} |
||||
|
.buttons { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
} |
||||
|
</style> |
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,4 +1,5 @@ |
|||||
export { organisation } from "./organisation" |
export { organisation } from "./organisation" |
||||
|
export { users } from "./users" |
||||
export { admin } from "./admin" |
export { admin } from "./admin" |
||||
export { apps } from "./apps" |
export { apps } from "./apps" |
||||
export { email } from "./email" |
export { email } from "./email" |
||||
|
|||||
@ -0,0 +1,65 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api, { post } from "builderStore/api" |
||||
|
import { update } from "lodash" |
||||
|
|
||||
|
export function createUsersStore() { |
||||
|
const { subscribe, set } = writable([]) |
||||
|
|
||||
|
async function init() { |
||||
|
const response = await api.get(`/api/admin/users`) |
||||
|
const json = await response.json() |
||||
|
set(json) |
||||
|
} |
||||
|
|
||||
|
async function invite(email) { |
||||
|
const response = await api.post(`/api/admin/users/invite`, { email }) |
||||
|
return await response.json() |
||||
|
} |
||||
|
async function acceptInvite(inviteCode, password) { |
||||
|
const response = await api.post("/api/admin/users/invite/accept", { |
||||
|
inviteCode, |
||||
|
password, |
||||
|
}) |
||||
|
return await response.json() |
||||
|
} |
||||
|
|
||||
|
async function create({ email, password }) { |
||||
|
const response = await api.post("/api/admin/users", { |
||||
|
email, |
||||
|
password, |
||||
|
builder: { global: true }, |
||||
|
roles: {}, |
||||
|
}) |
||||
|
init() |
||||
|
return await response.json() |
||||
|
} |
||||
|
|
||||
|
async function del(id) { |
||||
|
const response = await api.delete(`/api/admin/users/${id}`) |
||||
|
update(users => users.filter(user => user._id !== id)) |
||||
|
return await response.json() |
||||
|
} |
||||
|
|
||||
|
async function updateRoles(data) { |
||||
|
try { |
||||
|
const res = await post(`/api/admin/users`, data) |
||||
|
const json = await res.json() |
||||
|
return json |
||||
|
} catch (error) { |
||||
|
console.log(error) |
||||
|
return error |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe, |
||||
|
init, |
||||
|
invite, |
||||
|
acceptInvite, |
||||
|
create, |
||||
|
updateRoles, |
||||
|
del, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const users = createUsersStore() |
||||
@ -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