Merge branch 'master' of github.com:Budibase/budibase into develop
This commit is contained in:
commit
44a6aa85a9
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "1.1.33-alpha.4",
|
"@budibase/types": "^1.2.12",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
|
|
|
@ -19,6 +19,8 @@ const {
|
||||||
csrf,
|
csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
adminOnly,
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
|
|
||||||
|
@ -176,5 +178,7 @@ module.exports = {
|
||||||
updateUserOAuth,
|
updateUserOAuth,
|
||||||
ssoCallbackUrl,
|
ssoCallbackUrl,
|
||||||
adminOnly,
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ const env = {
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
SERVICE: process.env.SERVICE || "budibase",
|
SERVICE: process.env.SERVICE || "budibase",
|
||||||
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
DEPLOYMENT_ENVIRONMENT:
|
DEPLOYMENT_ENVIRONMENT:
|
||||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import auth from "./auth"
|
||||||
import constants from "./constants"
|
import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
import logging from "./logging"
|
import logging from "./logging"
|
||||||
|
import pino from "./pino"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -53,6 +54,7 @@ const core = {
|
||||||
errors,
|
errors,
|
||||||
logging,
|
logging,
|
||||||
roles,
|
roles,
|
||||||
|
...pino,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ module.exports = (
|
||||||
|
|
||||||
const session = await getSession(userId, sessionId)
|
const session = await getSession(userId, sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
error = "No session found"
|
error = `Session not found - ${userId} - ${sessionId}`
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
if (opts && opts.populateUser) {
|
if (opts && opts.populateUser) {
|
||||||
|
|
|
@ -10,6 +10,8 @@ const internalApi = require("./internalApi")
|
||||||
const datasourceGoogle = require("./passport/datasource/google")
|
const datasourceGoogle = require("./passport/datasource/google")
|
||||||
const csrf = require("./csrf")
|
const csrf = require("./csrf")
|
||||||
const adminOnly = require("./adminOnly")
|
const adminOnly = require("./adminOnly")
|
||||||
|
const builderOrAdmin = require("./builderOrAdmin")
|
||||||
|
const builderOnly = require("./builderOnly")
|
||||||
const joiValidator = require("./joi-validator")
|
const joiValidator = require("./joi-validator")
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
|
@ -27,5 +29,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
csrf,
|
csrf,
|
||||||
adminOnly,
|
adminOnly,
|
||||||
|
builderOnly,
|
||||||
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
||||||
type: MigrationType.INSTALLATION,
|
type: MigrationType.INSTALLATION,
|
||||||
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
|
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: MigrationType.GLOBAL,
|
||||||
|
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const env = require("./environment")
|
||||||
|
|
||||||
|
exports.pinoSettings = () => ({
|
||||||
|
prettyPrint: {
|
||||||
|
levelFirst: true,
|
||||||
|
},
|
||||||
|
level: env.LOG_LEVEL || "error",
|
||||||
|
autoLogging: {
|
||||||
|
ignore: req => req.url.includes("/health"),
|
||||||
|
},
|
||||||
|
})
|
|
@ -203,15 +203,24 @@ exports.getAllRoles = async appId => {
|
||||||
if (appId) {
|
if (appId) {
|
||||||
return doWithDB(appId, internal)
|
return doWithDB(appId, internal)
|
||||||
} else {
|
} else {
|
||||||
return internal(getAppDB())
|
let appDB
|
||||||
|
try {
|
||||||
|
appDB = getAppDB()
|
||||||
|
} catch (error) {
|
||||||
|
// We don't have any apps, so we'll just use the built-in roles
|
||||||
|
}
|
||||||
|
return internal(appDB)
|
||||||
}
|
}
|
||||||
async function internal(db) {
|
async function internal(db) {
|
||||||
const body = await db.allDocs(
|
let roles = []
|
||||||
getRoleParams(null, {
|
if (db) {
|
||||||
include_docs: true,
|
const body = await db.allDocs(
|
||||||
})
|
getRoleParams(null, {
|
||||||
)
|
include_docs: true,
|
||||||
let roles = body.rows.map(row => row.doc)
|
})
|
||||||
|
)
|
||||||
|
roles = body.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
const builtinRoles = exports.getBuiltinRoles()
|
const builtinRoles = exports.getBuiltinRoles()
|
||||||
|
|
||||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const redis = require("../redis/init")
|
const redis = require("../redis/init")
|
||||||
const { v4: uuidv4 } = require("uuid")
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
const { logWarn } = require("../logging")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
// a week in seconds
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
@ -33,12 +35,21 @@ async function invalidateSessions(userId, sessionIds = null) {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await redis.getSessionClient()
|
if (sessions && sessions.length > 0) {
|
||||||
const promises = []
|
const client = await redis.getSessionClient()
|
||||||
for (let session of sessions) {
|
const promises = []
|
||||||
promises.push(client.delete(session.key))
|
for (let session of sessions) {
|
||||||
|
promises.push(client.delete(session.key))
|
||||||
|
}
|
||||||
|
if (!env.isTest()) {
|
||||||
|
logWarn(
|
||||||
|
`Invalidating sessions for ${userId} - ${sessions
|
||||||
|
.map(session => session.key)
|
||||||
|
.join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error invalidating sessions: ${err}`)
|
console.error(`Error invalidating sessions: ${err}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "1.1.33-alpha.4",
|
"@budibase/string-templates": "^1.2.12",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
export let id = null
|
export let id = null
|
||||||
export let placeholder = "Choose an option or type"
|
export let placeholder = "Choose an option or type"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let readonly = false
|
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let error = null
|
export let error = null
|
||||||
export let secondaryOptions = []
|
export let secondaryOptions = []
|
||||||
|
@ -35,6 +34,7 @@
|
||||||
export let isOptionSelected = () => false
|
export let isOptionSelected = () => false
|
||||||
export let isPlaceholder = false
|
export let isPlaceholder = false
|
||||||
export let placeholderOption = null
|
export let placeholderOption = null
|
||||||
|
export let showClearIcon = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let primaryOpen = false
|
let primaryOpen = false
|
||||||
|
@ -50,17 +50,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValue = newValue => {
|
const updateValue = newValue => {
|
||||||
if (readonly) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickSecondary = () => {
|
const onClickSecondary = () => {
|
||||||
dispatch("click")
|
dispatch("click")
|
||||||
if (readonly) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
secondaryOpen = true
|
secondaryOpen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,24 +74,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = event => {
|
const onBlur = event => {
|
||||||
if (readonly) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
focus = false
|
focus = false
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = event => {
|
const onInput = event => {
|
||||||
if (readonly || !updateOnChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
if (readonly) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
@ -140,11 +125,12 @@
|
||||||
value={primaryLabel || ""}
|
value={primaryLabel || ""}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
readonly
|
||||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
class:labelPadding={iconData}
|
class:labelPadding={iconData}
|
||||||
|
class:open={primaryOpen}
|
||||||
/>
|
/>
|
||||||
{#if primaryValue}
|
{#if primaryValue && showClearIcon}
|
||||||
<button
|
<button
|
||||||
on:click={() => onClearPrimary()}
|
on:click={() => onClearPrimary()}
|
||||||
type="reset"
|
type="reset"
|
||||||
|
@ -198,7 +184,7 @@
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{#each groupTitles as title}
|
{#each groupTitles as title}
|
||||||
<div class="spectrum-Menu-item">
|
<div class="spectrum-Menu-item title">
|
||||||
<Detail>{title}</Detail>
|
<Detail>{title}</Detail>
|
||||||
</div>
|
</div>
|
||||||
{#if primaryOptions}
|
{#if primaryOptions}
|
||||||
|
@ -433,4 +419,18 @@
|
||||||
.spectrum-Search-clearButton {
|
.spectrum-Search-clearButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fix focus borders to show only when opened */
|
||||||
|
.spectrum-Textfield-input {
|
||||||
|
border-color: var(--spectrum-global-color-gray-400) !important;
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield-input.open {
|
||||||
|
border-color: var(--spectrum-global-color-blue-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix being able to hover and select titles */
|
||||||
|
.spectrum-Menu-item.title {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
export let primaryOptions = []
|
export let primaryOptions = []
|
||||||
export let secondaryOptions = []
|
export let secondaryOptions = []
|
||||||
export let searchTerm
|
export let searchTerm
|
||||||
|
export let showClearIcon = true
|
||||||
|
|
||||||
let primaryLabel
|
let primaryLabel
|
||||||
let secondaryLabel
|
let secondaryLabel
|
||||||
|
@ -120,6 +121,7 @@
|
||||||
{secondaryValue}
|
{secondaryValue}
|
||||||
{primaryLabel}
|
{primaryLabel}
|
||||||
{secondaryLabel}
|
{secondaryLabel}
|
||||||
|
{showClearIcon}
|
||||||
on:pickprimary={onPickPrimary}
|
on:pickprimary={onPickPrimary}
|
||||||
on:picksecondary={onPickSecondary}
|
on:picksecondary={onPickSecondary}
|
||||||
on:search={updateSearchTerm}
|
on:search={updateSearchTerm}
|
||||||
|
|
|
@ -9,11 +9,12 @@
|
||||||
export let avatar = false
|
export let avatar = false
|
||||||
export let title = null
|
export let title = null
|
||||||
export let subtitle = null
|
export let subtitle = null
|
||||||
|
export let hoverable = false
|
||||||
|
|
||||||
$: initials = avatar ? title?.[0] : null
|
$: initials = avatar ? title?.[0] : null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="list-item">
|
<div class="list-item" class:hoverable on:click>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<div class="icon" style="background: {iconBackground || `transparent`};">
|
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||||
|
@ -39,11 +40,12 @@
|
||||||
.list-item {
|
.list-item {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
background: var(--spectrum-global-color-gray-50);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
}
|
}
|
||||||
.list-item:not(:first-child) {
|
.list-item:not(:first-child) {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
@ -56,6 +58,10 @@
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.hoverable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-75);
|
||||||
|
}
|
||||||
.left,
|
.left,
|
||||||
.right {
|
.right {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -106,7 +106,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCancelButton}
|
{#if showCancelButton}
|
||||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
<Button group secondary newStyles on:click={close}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showConfirmButton}
|
{#if showConfirmButton}
|
||||||
<span class="confirm-wrap">
|
<span class="confirm-wrap">
|
||||||
|
|
|
@ -503,12 +503,6 @@
|
||||||
.spectrum-Table-headCell--alignRight {
|
.spectrum-Table-headCell--alignRight {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell--divider {
|
|
||||||
padding-right: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
|
|
||||||
padding-left: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-headCell--edit {
|
.spectrum-Table-headCell--edit {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -580,13 +574,6 @@
|
||||||
background-color: var(--table-bg);
|
background-color: var(--table-bg);
|
||||||
z-index: auto;
|
z-index: auto;
|
||||||
}
|
}
|
||||||
.spectrum-Table-cell--divider {
|
|
||||||
padding-right: var(--cell-padding);
|
|
||||||
}
|
|
||||||
.spectrum-Table-cell--divider + .spectrum-Table-cell {
|
|
||||||
padding-left: var(--cell-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spectrum-Table-cell--edit {
|
.spectrum-Table-cell--edit {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "1.1.33-alpha.4",
|
"@budibase/bbui": "^1.2.12",
|
||||||
"@budibase/client": "1.1.33-alpha.4",
|
"@budibase/client": "^1.2.12",
|
||||||
"@budibase/frontend-core": "1.1.33-alpha.4",
|
"@budibase/frontend-core": "^1.2.12",
|
||||||
"@budibase/string-templates": "1.1.33-alpha.4",
|
"@budibase/string-templates": "^1.2.12",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -14,7 +14,13 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
import {
|
||||||
|
Pagination,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -29,6 +35,13 @@
|
||||||
$: fetch = createFetch(id)
|
$: fetch = createFetch(id)
|
||||||
$: hasCols = checkHasCols(schema)
|
$: hasCols = checkHasCols(schema)
|
||||||
$: hasRows = !!$fetch.rows?.length
|
$: hasRows = !!$fetch.rows?.length
|
||||||
|
$: showError($fetch.error)
|
||||||
|
|
||||||
|
const showError = error => {
|
||||||
|
if (error) {
|
||||||
|
notifications.error(error?.message || "Unable to fetch data.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const enrichSchema = schema => {
|
const enrichSchema = schema => {
|
||||||
let tempSchema = { ...schema }
|
let tempSchema = { ...schema }
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
export let selectedRows
|
export let selectedRows
|
||||||
export let deleteRows
|
export let deleteRows
|
||||||
|
export let item = "row"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
@ -14,12 +15,14 @@
|
||||||
modal?.hide()
|
modal?.hide()
|
||||||
dispatch("updaterows")
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
||||||
Delete
|
Delete
|
||||||
{selectedRows.length}
|
{selectedRows.length}
|
||||||
row(s)
|
{text}
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={modal}
|
bind:this={modal}
|
||||||
|
@ -29,5 +32,5 @@
|
||||||
>
|
>
|
||||||
Are you sure you want to delete
|
Are you sure you want to delete
|
||||||
{selectedRows.length}
|
{selectedRows.length}
|
||||||
row{selectedRows.length > 1 ? "s" : ""}?
|
{text}?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import Logo from "assets/bb-space-man.svg"
|
import Logo from "assets/bb-space-man.svg"
|
||||||
import AccessFilter from "./_components/AcessFilter.svelte"
|
import AccessFilter from "./_components/AcessFilter.svelte"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
let sortBy = "name"
|
let sortBy = "name"
|
||||||
let template
|
let template
|
||||||
|
@ -69,10 +68,6 @@
|
||||||
$: unlocked = lockedApps?.length === 0
|
$: unlocked = lockedApps?.length === 0
|
||||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
|
@ -360,7 +355,7 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
{#if hasGroupsLicense && $groups.length}
|
{#if $auth.groupsEnabled && $groups.length}
|
||||||
<AccessFilter on:change={accessFilterAction} />
|
<AccessFilter on:change={accessFilterAction} />
|
||||||
{/if}
|
{/if}
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
|
|
||||||
$: wide =
|
$: wide =
|
||||||
$page.path.includes("email/:template") ||
|
$page.path.includes("email/:template") ||
|
||||||
($page.path.includes("users") && !$page.path.includes(":userId")) ||
|
|
||||||
($page.path.includes("groups") && !$page.path.includes(":groupId"))
|
($page.path.includes("groups") && !$page.path.includes(":groupId"))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { groups, auth } from "stores/portal"
|
import { groups, auth } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
|
||||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||||
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
|
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
@ -27,10 +26,6 @@
|
||||||
let modal
|
let modal
|
||||||
let group = cloneDeep(DefaultGroup)
|
let group = cloneDeep(DefaultGroup)
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
async function deleteGroup(group) {
|
async function deleteGroup(group) {
|
||||||
try {
|
try {
|
||||||
groups.actions.delete(group)
|
groups.actions.delete(group)
|
||||||
|
@ -54,7 +49,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
if (hasGroupsLicense) {
|
if ($auth.groupsEnabled) {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -67,7 +62,7 @@
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<Heading size="M">User groups</Heading>
|
<Heading size="M">User groups</Heading>
|
||||||
{#if !hasGroupsLicense}
|
{#if !$auth.groupsEnabled}
|
||||||
<Tags>
|
<Tags>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<div class="tag">
|
<div class="tag">
|
||||||
|
@ -82,15 +77,15 @@
|
||||||
<div class="align-buttons">
|
<div class="align-buttons">
|
||||||
<Button
|
<Button
|
||||||
newStyles
|
newStyles
|
||||||
icon={hasGroupsLicense ? "UserGroup" : ""}
|
icon={$auth.groupsEnabled ? "UserGroup" : ""}
|
||||||
cta={hasGroupsLicense}
|
cta={$auth.groupsEnabled}
|
||||||
on:click={hasGroupsLicense
|
on:click={$auth.groupsEnabled
|
||||||
? showCreateGroupModal
|
? showCreateGroupModal
|
||||||
: window.open("https://budibase.com/pricing/", "_blank")}
|
: window.open("https://budibase.com/pricing/", "_blank")}
|
||||||
>
|
>
|
||||||
{hasGroupsLicense ? "Create user group" : "Upgrade Account"}
|
{$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
|
||||||
</Button>
|
</Button>
|
||||||
{#if !hasGroupsLicense}
|
{#if !$auth.groupsEnabled}
|
||||||
<Button
|
<Button
|
||||||
newStyles
|
newStyles
|
||||||
secondary
|
secondary
|
||||||
|
@ -101,7 +96,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if hasGroupsLicense && $groups.length}
|
{#if $auth.groupsEnabled && $groups.length}
|
||||||
<div class="groupTable">
|
<div class="groupTable">
|
||||||
{#each $groups as group}
|
{#each $groups as group}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
Select,
|
Select,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
|
Divider,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
@ -41,18 +42,13 @@
|
||||||
let allAppList = []
|
let allAppList = []
|
||||||
let user
|
let user
|
||||||
let loaded = false
|
let loaded = false
|
||||||
$: fetchUser(userId)
|
|
||||||
|
|
||||||
|
$: fetchUser(userId)
|
||||||
$: fullName = $userFetch?.data?.firstName
|
$: fullName = $userFetch?.data?.firstName
|
||||||
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
|
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
$: nameLabel = getNameLabel($userFetch)
|
$: nameLabel = getNameLabel($userFetch)
|
||||||
$: initials = getInitials(nameLabel)
|
$: initials = getInitials(nameLabel)
|
||||||
|
|
||||||
$: allAppList = $apps
|
$: allAppList = $apps
|
||||||
.filter(x => {
|
.filter(x => {
|
||||||
if ($userFetch.data?.roles) {
|
if ($userFetch.data?.roles) {
|
||||||
|
@ -85,7 +81,6 @@
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
$: globalRole = $userFetch?.data?.admin?.global
|
$: globalRole = $userFetch?.data?.admin?.global
|
||||||
? "admin"
|
? "admin"
|
||||||
: $userFetch?.data?.builder?.global
|
: $userFetch?.data?.builder?.global
|
||||||
|
@ -216,15 +211,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<Layout gap="L" noPadding>
|
<Layout gap="XL" noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<div>
|
||||||
<div>
|
<ActionButton on:click={() => $goto("./")} icon="ArrowLeft">
|
||||||
<ActionButton on:click={() => $goto("./")} size="S" icon="ArrowLeft">
|
Back
|
||||||
Back
|
</ActionButton>
|
||||||
</ActionButton>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
<Layout noPadding gap="M">
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
|
@ -232,74 +226,83 @@
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
<Heading size="S">{nameLabel}</Heading>
|
<Heading size="S">{nameLabel}</Heading>
|
||||||
{#if nameLabel !== $userFetch?.data?.email}
|
{#if nameLabel !== $userFetch?.data?.email}
|
||||||
<Body size="XS">{$userFetch?.data?.email}</Body>
|
<Body size="S">{$userFetch?.data?.email}</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<ActionMenu align="right">
|
|
||||||
<span slot="control">
|
|
||||||
<Icon hoverable name="More" />
|
|
||||||
</span>
|
|
||||||
<MenuItem on:click={resetPasswordModal.show} icon="Refresh"
|
|
||||||
>Force Password Reset</MenuItem
|
|
||||||
>
|
|
||||||
<MenuItem on:click={deleteModal.show} icon="Delete">Delete</MenuItem
|
|
||||||
>
|
|
||||||
</ActionMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<div class="fields">
|
|
||||||
<div class="field">
|
|
||||||
<Label size="L">First name</Label>
|
|
||||||
<Input
|
|
||||||
thin
|
|
||||||
value={$userFetch?.data?.firstName}
|
|
||||||
on:blur={updateUserFirstName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<Label size="L">Last name</Label>
|
|
||||||
<Input
|
|
||||||
thin
|
|
||||||
value={$userFetch?.data?.lastName}
|
|
||||||
on:blur={updateUserLastName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
|
||||||
{#if userId !== $auth.user._id}
|
{#if userId !== $auth.user._id}
|
||||||
<div class="field">
|
<div>
|
||||||
<Label size="L">Role</Label>
|
<ActionMenu align="right">
|
||||||
<Select
|
<span slot="control">
|
||||||
value={globalRole}
|
<Icon hoverable name="More" />
|
||||||
options={Constants.BbRoles}
|
</span>
|
||||||
on:change={updateUserRole}
|
<MenuItem on:click={resetPasswordModal.show} icon="Refresh">
|
||||||
/>
|
Force password reset
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem on:click={deleteModal.show} icon="Delete">
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Heading size="S">Details</Heading>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Email</Label>
|
||||||
|
<Input disabled value={$userFetch?.data?.email} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">First name</Label>
|
||||||
|
<Input
|
||||||
|
value={$userFetch?.data?.firstName}
|
||||||
|
on:blur={updateUserFirstName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Last name</Label>
|
||||||
|
<Input
|
||||||
|
value={$userFetch?.data?.lastName}
|
||||||
|
on:blur={updateUserLastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
|
{#if userId !== $auth.user._id}
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Role</Label>
|
||||||
|
<Select
|
||||||
|
value={globalRole}
|
||||||
|
options={Constants.BudibaseRoleOptions}
|
||||||
|
on:change={updateUserRole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{#if hasGroupsLicense}
|
{#if $auth.groupsEnabled}
|
||||||
<!-- User groups -->
|
<!-- User groups -->
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
<div class="tableTitle">
|
<div class="tableTitle">
|
||||||
<div>
|
<Heading size="S">User groups</Heading>
|
||||||
<Heading size="XS">User groups</Heading>
|
|
||||||
<Body size="S">Add or remove this user from user groups</Body>
|
|
||||||
</div>
|
|
||||||
<div bind:this={popoverAnchor}>
|
<div bind:this={popoverAnchor}>
|
||||||
<Button on:click={popover.show()} icon="UserGroup" cta>
|
<Button
|
||||||
Add user group
|
on:click={popover.show()}
|
||||||
|
icon="UserGroup"
|
||||||
|
secondary
|
||||||
|
newStyles
|
||||||
|
>
|
||||||
|
Add to user group
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||||
<UserGroupPicker
|
<UserGroupPicker
|
||||||
key={"name"}
|
key={"name"}
|
||||||
title={"Group"}
|
title={"User group"}
|
||||||
bind:searchTerm
|
bind:searchTerm
|
||||||
bind:selected={selectedGroups}
|
bind:selected={selectedGroups}
|
||||||
bind:filtered={filteredGroups}
|
bind:filtered={filteredGroups}
|
||||||
|
@ -308,7 +311,6 @@
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{#if userGroups.length}
|
{#if userGroups.length}
|
||||||
{#each userGroups as group}
|
{#each userGroups as group}
|
||||||
|
@ -316,13 +318,16 @@
|
||||||
title={group.name}
|
title={group.name}
|
||||||
icon={group.icon}
|
icon={group.icon}
|
||||||
iconBackground={group.color}
|
iconBackground={group.color}
|
||||||
><Icon
|
hoverable
|
||||||
|
on:click={() => $goto(`../groups/${group._id}`)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
on:click={removeGroup(group._id)}
|
on:click={removeGroup(group._id)}
|
||||||
hoverable
|
hoverable
|
||||||
size="L"
|
size="S"
|
||||||
name="Close"
|
name="Close"
|
||||||
/></ListItem
|
/>
|
||||||
>
|
</ListItem>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<ListItem icon="UserGroup" title="No groups" />
|
<ListItem icon="UserGroup" title="No groups" />
|
||||||
|
@ -330,37 +335,28 @@
|
||||||
</List>
|
</List>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- User Apps -->
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<div class="appsTitle">
|
|
||||||
<Heading weight="light" size="XS">Apps</Heading>
|
|
||||||
<div style="margin-top: var(--spacing-xs)">
|
|
||||||
<Body size="S">Manage apps that this user has been assigned to</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Heading size="S">Apps</Heading>
|
||||||
<List>
|
<List>
|
||||||
{#if allAppList.length}
|
{#if allAppList.length}
|
||||||
{#each allAppList as app}
|
{#each allAppList as app}
|
||||||
<div
|
<ListItem
|
||||||
class="pointer"
|
title={app.name}
|
||||||
on:click={$goto(`../../overview/${app.devId}`)}
|
iconBackground={app?.icon?.color || ""}
|
||||||
|
icon={app?.icon?.name || "Apps"}
|
||||||
|
hoverable
|
||||||
|
on:click={() => $goto(`../../overview/${app.devId}`)}
|
||||||
>
|
>
|
||||||
<ListItem
|
<div class="title ">
|
||||||
title={app.name}
|
<StatusLight
|
||||||
iconBackground={app?.icon?.color || ""}
|
square
|
||||||
icon={app?.icon?.name || "Apps"}
|
color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
|
||||||
>
|
>
|
||||||
<div class="title ">
|
{getRoleLabel(getHighestRole(app.roles))}
|
||||||
<StatusLight
|
</StatusLight>
|
||||||
square
|
</div>
|
||||||
color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
|
</ListItem>
|
||||||
>
|
|
||||||
{getRoleLabel(getHighestRole(app.roles))}
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<ListItem icon="Apps" title="No apps" />
|
<ListItem icon="Apps" title="No apps" />
|
||||||
|
@ -381,16 +377,13 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32% 1fr;
|
grid-template-columns: 120px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,7 +396,7 @@
|
||||||
.tableTitle {
|
.tableTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: var(--spacing-m);
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
|
@ -413,9 +406,4 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appsTitle {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,13 +13,10 @@
|
||||||
import { emailValidator } from "helpers/validation"
|
import { emailValidator } from "helpers/validation"
|
||||||
|
|
||||||
export let showOnboardingTypeModal
|
export let showOnboardingTypeModal
|
||||||
|
|
||||||
const password = Math.random().toString(36).substring(2, 22)
|
const password = Math.random().toString(36).substring(2, 22)
|
||||||
let disabled
|
let disabled
|
||||||
let userGroups = []
|
let userGroups = []
|
||||||
$: errors = []
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
$: userData = [
|
$: userData = [
|
||||||
{
|
{
|
||||||
|
@ -29,6 +26,7 @@
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
function removeInput(idx) {
|
function removeInput(idx) {
|
||||||
userData = userData.filter((e, i) => i !== idx)
|
userData = userData.filter((e, i) => i !== idx)
|
||||||
|
@ -41,38 +39,49 @@
|
||||||
role: "appUser",
|
role: "appUser",
|
||||||
password: Math.random().toString(36).substring(2, 22),
|
password: Math.random().toString(36).substring(2, 22),
|
||||||
forceResetPassword: true,
|
forceResetPassword: true,
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateInput(email, index) {
|
function validateInput(email, index) {
|
||||||
if (email) {
|
if (email) {
|
||||||
if (emailValidator(email) === true) {
|
const res = emailValidator(email)
|
||||||
errors[index] = true
|
if (res === true) {
|
||||||
return null
|
delete userData[index].error
|
||||||
} else {
|
} else {
|
||||||
errors[index] = false
|
userData[index].error = res
|
||||||
return emailValidator(email)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
userData[index].error = "Please enter an email address"
|
||||||
}
|
}
|
||||||
|
return userData[index].error == null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
let valid = true
|
||||||
|
userData.forEach((input, index) => {
|
||||||
|
valid = validateInput(input.email, index) && valid
|
||||||
|
})
|
||||||
|
if (!valid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
showOnboardingTypeModal({ users: userData, groups: userGroups })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={async () =>
|
{onConfirm}
|
||||||
showOnboardingTypeModal({ users: userData, groups: userGroups })}
|
|
||||||
size="M"
|
size="M"
|
||||||
title="Add new user"
|
title="Add new users"
|
||||||
confirmText="Add user"
|
confirmText="Add users"
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
disabled={errors.some(x => x === false) ||
|
disabled={hasError || !userData.length}
|
||||||
userData.some(x => x.email === "" || x.email === null)}
|
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Email Address</Label>
|
<Label>Email address</Label>
|
||||||
|
|
||||||
{#each userData as input, index}
|
{#each userData as input, index}
|
||||||
<div
|
<div
|
||||||
style="display: flex;
|
style="display: flex;
|
||||||
|
@ -84,15 +93,12 @@
|
||||||
inputType="email"
|
inputType="email"
|
||||||
bind:inputValue={input.email}
|
bind:inputValue={input.email}
|
||||||
bind:dropdownValue={input.role}
|
bind:dropdownValue={input.role}
|
||||||
options={Constants.BbRoles}
|
options={Constants.BudibaseRoleOptions}
|
||||||
error={validateInput(input.email, index)}
|
error={input.error}
|
||||||
|
on:blur={() => validateInput(input.email, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="icon">
|
||||||
class:fix-height={errors.length && !errors[index]}
|
|
||||||
class:normal-height={errors.length && !!errors[index]}
|
|
||||||
style="width: 10% "
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="Close"
|
name="Close"
|
||||||
hoverable
|
hoverable
|
||||||
|
@ -107,11 +113,11 @@
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{#if hasGroupsLicense}
|
{#if $auth.groupsEnabled}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={userGroups}
|
bind:value={userGroups}
|
||||||
placeholder="Select User Groups"
|
placeholder="No groups"
|
||||||
label="User Groups"
|
label="Groups"
|
||||||
options={$groups}
|
options={$groups}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
getOptionValue={option => option._id}
|
getOptionValue={option => option._id}
|
||||||
|
@ -120,10 +126,9 @@
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fix-height {
|
.icon {
|
||||||
margin-bottom: 5%;
|
width: 10%;
|
||||||
}
|
align-self: flex-start;
|
||||||
.normal-height {
|
margin-top: 8px;
|
||||||
margin-bottom: 0%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Body } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -7,17 +8,9 @@
|
||||||
<div class="spacing">
|
<div class="spacing">
|
||||||
<Icon name="UserGroup" />
|
<Icon name="UserGroup" />
|
||||||
</div>
|
</div>
|
||||||
{#if value?.length === 0}
|
<div class="opacity">
|
||||||
<div class="opacity">0</div>
|
{value?.length || 0}
|
||||||
{:else if value?.length === 1}
|
</div>
|
||||||
<div class="opacity">
|
|
||||||
<Body size="S">{value[0]?.name}</Body>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="opacity">
|
|
||||||
{parseInt(value?.length) || 0} groups
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { groups, auth, admin } from "stores/portal"
|
import { groups, auth, admin } from "stores/portal"
|
||||||
import { emailValidator } from "../../../../../../helpers/validation"
|
import { emailValidator } from "helpers/validation"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
@ -22,9 +22,6 @@
|
||||||
let usersRole = null
|
let usersRole = null
|
||||||
|
|
||||||
$: invalidEmails = []
|
$: invalidEmails = []
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -81,7 +78,7 @@
|
||||||
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
|
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
|
||||||
disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
|
disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
|
||||||
>
|
>
|
||||||
<Body size="S">Import your users email addrresses from a CSV</Body>
|
<Body size="S">Import your users email addresses from a CSV file</Body>
|
||||||
|
|
||||||
<div class="dropzone">
|
<div class="dropzone">
|
||||||
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||||
|
@ -95,11 +92,11 @@
|
||||||
options={Constants.BuilderRoleDescriptions}
|
options={Constants.BuilderRoleDescriptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if hasGroupsLicense}
|
{#if $auth.groupsEnabled}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={userGroups}
|
bind:value={userGroups}
|
||||||
placeholder="Select User Groups"
|
placeholder="No groups"
|
||||||
label="User Groups"
|
label="Groups"
|
||||||
options={$groups}
|
options={$groups}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
getOptionValue={option => option._id}
|
getOptionValue={option => option._id}
|
||||||
|
@ -122,14 +119,12 @@
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
transition: all 0.2s ease 0s;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
|
@ -141,10 +136,15 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--grey-2);
|
background: var(--spectrum-global-color-gray-200);
|
||||||
font-size: var(--font-size-xs);
|
font-size: 12px;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
border: var(--border-transparent);
|
border: var(--border-transparent);
|
||||||
|
transition: background-color 130ms ease-out;
|
||||||
|
}
|
||||||
|
label:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
|
|
|
@ -49,10 +49,10 @@
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body size="XS"
|
<Body size="XS">
|
||||||
>All your new users can be accessed through the autogenerated passwords.
|
All your new users can be accessed through the autogenerated passwords. Take
|
||||||
Make not of these passwords or download the csv</Body
|
note of these passwords or download the CSV file.
|
||||||
>
|
</Body>
|
||||||
|
|
||||||
<div class="container" on:click={downloadCsvFile}>
|
<div class="container" on:click={downloadCsvFile}>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|
|
@ -3,14 +3,20 @@
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
$: value =
|
|
||||||
Constants.BbRoles.find(x => x.value === users.getUserRole(row))?.label ||
|
const TooltipMap = {
|
||||||
"Not Available"
|
appUser: "Only has access to published apps",
|
||||||
|
developer: "Access to the app builder",
|
||||||
|
admin: "Full access",
|
||||||
|
}
|
||||||
|
|
||||||
|
$: role = Constants.BudibaseRoleOptions.find(
|
||||||
|
x => x.value === users.getUserRole(row)
|
||||||
|
)
|
||||||
|
$: value = role?.label || "Not available"
|
||||||
|
$: tooltip = TooltipMap[role?.value] || ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click|stopPropagation>
|
<div on:click|stopPropagation title={tooltip}>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -8,11 +8,10 @@
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Icon,
|
Search,
|
||||||
notifications,
|
notifications,
|
||||||
Pagination,
|
Pagination,
|
||||||
Search,
|
Divider,
|
||||||
Label,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AddUserModal from "./_components/AddUserModal.svelte"
|
import AddUserModal from "./_components/AddUserModal.svelte"
|
||||||
import { users, groups, auth } from "stores/portal"
|
import { users, groups, auth } from "stores/portal"
|
||||||
|
@ -20,68 +19,42 @@
|
||||||
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
|
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
|
||||||
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
|
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
|
||||||
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
|
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
|
||||||
import NameTableRenderer from "./_components/NameTableRenderer.svelte"
|
|
||||||
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
|
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const accessTypes = [
|
|
||||||
{
|
|
||||||
icon: "User",
|
|
||||||
description: "App user - Only has access to published apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "Hammer",
|
|
||||||
description: "Developer - Access to the app builder",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "Draw",
|
|
||||||
description: "Admin - Full access",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
//let email
|
|
||||||
let enrichedUsers = []
|
let enrichedUsers = []
|
||||||
let createUserModal,
|
let createUserModal,
|
||||||
inviteConfirmationModal,
|
inviteConfirmationModal,
|
||||||
onboardingTypeModal,
|
onboardingTypeModal,
|
||||||
passwordModal,
|
passwordModal,
|
||||||
importUsersModal
|
importUsersModal
|
||||||
|
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let prevEmail = undefined,
|
let prevEmail = undefined,
|
||||||
searchEmail = undefined
|
searchEmail = undefined
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let customRenderers = [
|
let customRenderers = [
|
||||||
{ column: "userGroups", component: GroupsTableRenderer },
|
{ column: "userGroups", component: GroupsTableRenderer },
|
||||||
{ column: "apps", component: AppsTableRenderer },
|
{ column: "apps", component: AppsTableRenderer },
|
||||||
{ column: "name", component: NameTableRenderer },
|
|
||||||
{ column: "role", component: RoleTableRenderer },
|
{ column: "role", component: RoleTableRenderer },
|
||||||
]
|
]
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
$: schema = {
|
$: schema = {
|
||||||
name: {},
|
|
||||||
email: {},
|
email: {},
|
||||||
role: {
|
role: {
|
||||||
sortable: false,
|
sortable: false,
|
||||||
},
|
},
|
||||||
...(hasGroupsLicense && {
|
...($auth.groupsEnabled && {
|
||||||
userGroups: { sortable: false, displayName: "User groups" },
|
userGroups: { sortable: false, displayName: "Groups" },
|
||||||
}),
|
}),
|
||||||
apps: {},
|
apps: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
$: userData = []
|
$: userData = []
|
||||||
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, searchEmail)
|
$: fetchUsers(page, searchEmail)
|
||||||
$: {
|
$: {
|
||||||
|
@ -104,6 +77,7 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showOnboardingTypeModal = async addUsersData => {
|
const showOnboardingTypeModal = async addUsersData => {
|
||||||
userData = await removingDuplicities(addUsersData)
|
userData = await removingDuplicities(addUsersData)
|
||||||
if (!userData?.users?.length) return
|
if (!userData?.users?.length) return
|
||||||
|
@ -112,13 +86,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUserFlow() {
|
async function createUserFlow() {
|
||||||
let emails = userData?.users?.map(x => x.email) || []
|
const payload = userData?.users?.map(user => ({
|
||||||
|
email: user.email,
|
||||||
|
builder: user.role === Constants.BudibaseRoles.Developer,
|
||||||
|
admin: user.role === Constants.BudibaseRoles.Admin,
|
||||||
|
}))
|
||||||
try {
|
try {
|
||||||
const res = await users.invite({
|
const res = await users.invite(payload)
|
||||||
emails: emails,
|
|
||||||
builder: false,
|
|
||||||
admin: false,
|
|
||||||
})
|
|
||||||
notifications.success(res.message)
|
notifications.success(res.message)
|
||||||
inviteConfirmationModal.show()
|
inviteConfirmationModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -198,6 +172,10 @@
|
||||||
const deleteRows = async () => {
|
const deleteRows = async () => {
|
||||||
try {
|
try {
|
||||||
let ids = selectedRows.map(user => user._id)
|
let ids = selectedRows.map(user => user._id)
|
||||||
|
if (ids.includes(get(auth).user._id)) {
|
||||||
|
notifications.error("You cannot delete yourself")
|
||||||
|
return
|
||||||
|
}
|
||||||
await users.bulkDelete(ids)
|
await users.bulkDelete(ids)
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
|
@ -227,23 +205,13 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding gap="M">
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading>Users</Heading>
|
<Heading>Users</Heading>
|
||||||
<Body>Add users and control who gets access to your published apps</Body>
|
<Body>Add users and control who gets access to your published apps</Body>
|
||||||
|
|
||||||
<div>
|
|
||||||
{#each accessTypes as type}
|
|
||||||
<div class="access-description">
|
|
||||||
<Icon name={type.icon} />
|
|
||||||
<div class="access-text">
|
|
||||||
<Body size="S">{type.description}</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="S" noPadding>
|
<Divider size="S" />
|
||||||
|
<div class="controls">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
dataCy="add-user"
|
dataCy="add-user"
|
||||||
|
@ -251,39 +219,47 @@
|
||||||
icon="UserAdd"
|
icon="UserAdd"
|
||||||
cta>Add users</Button
|
cta>Add users</Button
|
||||||
>
|
>
|
||||||
<Button on:click={importUsersModal.show} icon="Import" primary
|
<Button
|
||||||
>Import users</Button
|
on:click={importUsersModal.show}
|
||||||
|
icon="Import"
|
||||||
|
secondary
|
||||||
|
newStyles
|
||||||
>
|
>
|
||||||
|
Import users
|
||||||
<div class="field">
|
</Button>
|
||||||
<Label size="L">Search email</Label>
|
|
||||||
<Search bind:value={searchEmail} placeholder="" />
|
|
||||||
</div>
|
|
||||||
{#if selectedRows.length > 0}
|
|
||||||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
|
||||||
{/if}
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Table
|
<div class="controls-right">
|
||||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
<Search bind:value={searchEmail} placeholder="Search email" />
|
||||||
{schema}
|
{#if selectedRows.length > 0}
|
||||||
bind:selectedRows
|
<DeleteRowsButton
|
||||||
data={enrichedUsers}
|
item="user"
|
||||||
allowEditColumns={false}
|
on:updaterows
|
||||||
allowEditRows={false}
|
{selectedRows}
|
||||||
allowSelectRows={true}
|
{deleteRows}
|
||||||
showHeaderBorder={false}
|
/>
|
||||||
{customRenderers}
|
{/if}
|
||||||
/>
|
|
||||||
<div class="pagination">
|
|
||||||
<Pagination
|
|
||||||
page={$pageInfo.pageNumber}
|
|
||||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
|
||||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
|
||||||
goToPrevPage={pageInfo.prevPage}
|
|
||||||
goToNextPage={pageInfo.nextPage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
|
<Table
|
||||||
|
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||||
|
{schema}
|
||||||
|
bind:selectedRows
|
||||||
|
data={enrichedUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={true}
|
||||||
|
showHeaderBorder={false}
|
||||||
|
{customRenderers}
|
||||||
|
/>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$pageInfo.pageNumber}
|
||||||
|
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||||
|
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||||
|
goToPrevPage={pageInfo.prevPage}
|
||||||
|
goToNextPage={pageInfo.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
|
@ -320,28 +296,22 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
grid-gap: var(--spacing-m);
|
justify-content: space-between;
|
||||||
margin-left: auto;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.controls-right {
|
||||||
.field > :global(*) + :global(*) {
|
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.access-description {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: var(--spacing-xl);
|
flex-direction: row;
|
||||||
opacity: 0.8;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.controls-right :global(.spectrum-Search) {
|
||||||
.access-text {
|
width: 200px;
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
import { users, groups, apps, auth } from "stores/portal"
|
import { users, groups, apps, auth } from "stores/portal"
|
||||||
import AssignmentModal from "./AssignmentModal.svelte"
|
import AssignmentModal from "./AssignmentModal.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
let assignmentModal
|
let assignmentModal
|
||||||
let appGroups = []
|
let appGroups = []
|
||||||
let appUsers = []
|
let appUsers = []
|
||||||
|
@ -28,14 +28,9 @@
|
||||||
search = undefined
|
search = undefined
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let fixedAppId
|
let fixedAppId
|
||||||
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
|
|
||||||
$: hasGroupsLicense = $auth.user?.license.features.includes(
|
|
||||||
Constants.Features.USER_GROUPS
|
|
||||||
)
|
|
||||||
|
|
||||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||||
|
|
||||||
$: appGroups = $groups.filter(x => {
|
$: appGroups = $groups.filter(x => {
|
||||||
return x.apps.includes(app.appId)
|
return x.apps.includes(app.appId)
|
||||||
})
|
})
|
||||||
|
@ -161,7 +156,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if hasGroupsLicense && appGroups.length}
|
{#if $auth.groupsEnabled && appGroups.length}
|
||||||
<List title="User Groups">
|
<List title="User Groups">
|
||||||
{#each appGroups as group}
|
{#each appGroups as group}
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|
|
@ -4,22 +4,65 @@
|
||||||
PickerDropdown,
|
PickerDropdown,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Layout,
|
Layout,
|
||||||
|
Icon,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { groups, users } from "stores/portal"
|
import { groups, users, auth } from "stores/portal"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let addData
|
export let addData
|
||||||
export let appUsers = []
|
export let appUsers = []
|
||||||
|
|
||||||
let prevSearch = undefined,
|
let prevSearch = undefined,
|
||||||
search = undefined
|
search = undefined
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
|
let appData = [{ id: "", role: "" }]
|
||||||
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, search)
|
$: fetchUsers(page, search)
|
||||||
|
$: availableUsers = getAvailableUsers($users, appUsers, appData)
|
||||||
|
$: filteredGroups = $groups.filter(group => {
|
||||||
|
return !group.apps.find(appId => {
|
||||||
|
return appId === app.appId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
$: valid =
|
||||||
|
appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length)
|
||||||
|
$: optionSections = {
|
||||||
|
...($auth.groupsEnabled &&
|
||||||
|
filteredGroups.length && {
|
||||||
|
["User groups"]: {
|
||||||
|
data: filteredGroups,
|
||||||
|
getLabel: group => group.name,
|
||||||
|
getValue: group => group._id,
|
||||||
|
getIcon: group => group.icon,
|
||||||
|
getColour: group => group.color,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
users: {
|
||||||
|
data: availableUsers,
|
||||||
|
getLabel: user => user.email,
|
||||||
|
getValue: user => user._id,
|
||||||
|
getIcon: user => user.icon,
|
||||||
|
getColour: user => user.color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
|
||||||
|
return (allUsers.data || []).filter(user => {
|
||||||
|
// Filter out assigned users
|
||||||
|
if (appUsers.find(x => x._id === user._id)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out new users which are going to be assigned
|
||||||
|
return !newUsers.find(x => x.id === user._id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchUsers(page, search) {
|
async function fetchUsers(page, search) {
|
||||||
if ($pageInfo.loading) {
|
if ($pageInfo.loading) {
|
||||||
return
|
return
|
||||||
|
@ -39,36 +82,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filteredGroups = $groups.filter(group => {
|
|
||||||
return !group.apps.find(appId => {
|
|
||||||
return appId === app.appId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
$: optionSections = {
|
|
||||||
...(filteredGroups.length && {
|
|
||||||
groups: {
|
|
||||||
data: filteredGroups,
|
|
||||||
getLabel: group => group.name,
|
|
||||||
getValue: group => group._id,
|
|
||||||
getIcon: group => group.icon,
|
|
||||||
getColour: group => group.color,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
users: {
|
|
||||||
data: $users.data.filter(u => !appUsers.find(x => x._id === u._id)),
|
|
||||||
getLabel: user => user.email,
|
|
||||||
getValue: user => user._id,
|
|
||||||
getIcon: user => user.icon,
|
|
||||||
getColour: user => user.color,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
$: appData = [{ id: "", role: "" }]
|
|
||||||
|
|
||||||
function addNewInput() {
|
function addNewInput() {
|
||||||
appData = [...appData, { id: "", role: "" }]
|
appData = [...appData, { id: "", role: "" }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeItem = index => {
|
||||||
|
appData = appData.filter((x, idx) => idx !== index)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -78,28 +98,61 @@
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
onConfirm={() => addData(appData)}
|
onConfirm={() => addData(appData)}
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
|
disabled={!valid}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
{#if appData?.length}
|
||||||
{#each appData as input, index}
|
<Layout noPadding gap="XS">
|
||||||
<PickerDropdown
|
{#each appData as input, index}
|
||||||
autocomplete
|
<div class="item">
|
||||||
primaryOptions={optionSections}
|
<div class="picker">
|
||||||
secondaryOptions={$roles}
|
<PickerDropdown
|
||||||
secondaryPlaceholder="Access"
|
autocomplete
|
||||||
bind:primaryValue={input.id}
|
showClearIcon={false}
|
||||||
bind:secondaryValue={input.role}
|
primaryOptions={optionSections}
|
||||||
bind:searchTerm={search}
|
secondaryOptions={$roles}
|
||||||
getPrimaryOptionLabel={group => group.name}
|
secondaryPlaceholder="Access"
|
||||||
getPrimaryOptionValue={group => group.name}
|
bind:primaryValue={input.id}
|
||||||
getPrimaryOptionIcon={group => group.icon}
|
bind:secondaryValue={input.role}
|
||||||
getPrimaryOptionColour={group => group.colour}
|
bind:searchTerm={search}
|
||||||
getSecondaryOptionLabel={role => role.name}
|
getPrimaryOptionLabel={group => group.name}
|
||||||
getSecondaryOptionValue={role => role._id}
|
getPrimaryOptionValue={group => group.name}
|
||||||
getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
getPrimaryOptionIcon={group => group.icon}
|
||||||
/>
|
getPrimaryOptionColour={group => group.colour}
|
||||||
{/each}
|
getSecondaryOptionLabel={role => role.name}
|
||||||
</Layout>
|
getSecondaryOptionValue={role => role._id}
|
||||||
|
getSecondaryOptionColour={role =>
|
||||||
|
RoleUtils.getRoleColour(role._id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<Icon
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => removeItem(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.picker {
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { derived, writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
import { FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export function createAuthStore() {
|
export function createAuthStore() {
|
||||||
const auth = writable({
|
const auth = writable({
|
||||||
|
@ -10,11 +12,13 @@ export function createAuthStore() {
|
||||||
tenantSet: false,
|
tenantSet: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
postLogout: false,
|
postLogout: false,
|
||||||
|
groupsEnabled: false,
|
||||||
})
|
})
|
||||||
const store = derived(auth, $store => {
|
const store = derived(auth, $store => {
|
||||||
let initials = null
|
let initials = null
|
||||||
let isAdmin = false
|
let isAdmin = false
|
||||||
let isBuilder = false
|
let isBuilder = false
|
||||||
|
let groupsEnabled = false
|
||||||
if ($store.user) {
|
if ($store.user) {
|
||||||
const user = $store.user
|
const user = $store.user
|
||||||
if (user.firstName) {
|
if (user.firstName) {
|
||||||
|
@ -29,6 +33,9 @@ export function createAuthStore() {
|
||||||
}
|
}
|
||||||
isAdmin = !!user.admin?.global
|
isAdmin = !!user.admin?.global
|
||||||
isBuilder = !!user.builder?.global
|
isBuilder = !!user.builder?.global
|
||||||
|
groupsEnabled =
|
||||||
|
user?.license.features.includes(Constants.Features.USER_GROUPS) &&
|
||||||
|
user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
user: $store.user,
|
user: $store.user,
|
||||||
|
@ -39,6 +46,7 @@ export function createAuthStore() {
|
||||||
initials,
|
initials,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isBuilder,
|
isBuilder,
|
||||||
|
groupsEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,8 @@ export function createUsersStore() {
|
||||||
return await API.getUsers()
|
return await API.getUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invite({ emails, builder, admin }) {
|
async function invite(payload) {
|
||||||
return API.inviteUsers({
|
return API.inviteUsers(payload)
|
||||||
emails,
|
|
||||||
builder,
|
|
||||||
admin,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
|
|
|
@ -5,3 +5,4 @@ build/
|
||||||
docker-error.log
|
docker-error.log
|
||||||
envoy.yaml
|
envoy.yaml
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
prebuilds/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "1.1.33-alpha.4",
|
"@budibase/bbui": "^1.2.12",
|
||||||
"@budibase/frontend-core": "1.1.33-alpha.4",
|
"@budibase/frontend-core": "^1.2.12",
|
||||||
"@budibase/string-templates": "1.1.33-alpha.4",
|
"@budibase/string-templates": "^1.2.12",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "1.1.33-alpha.4",
|
"@budibase/bbui": "^1.2.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,20 +141,18 @@ export const buildUserEndpoints = API => ({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple users to the current tenant.
|
* Invites multiple users to the current tenant.
|
||||||
* @param email An array of email addresses
|
* @param users An array of users to invite
|
||||||
* @param builder whether the user should be a global builder
|
|
||||||
* @param admin whether the user should be a global admin
|
|
||||||
*/
|
*/
|
||||||
inviteUsers: async ({ emails, builder, admin }) => {
|
inviteUsers: async users => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/global/users/inviteMultiple",
|
url: "/api/global/users/multi/invite",
|
||||||
body: {
|
body: users.map(user => ({
|
||||||
emails,
|
email: user.email,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
admin: admin ? { global: true } : undefined,
|
admin: user.admin ? { global: true } : undefined,
|
||||||
builder: builder ? { global: true } : undefined,
|
builder: user.admin || user.builder ? { global: true } : undefined,
|
||||||
},
|
},
|
||||||
},
|
})),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -60,25 +60,31 @@ export const TableNames = {
|
||||||
USERS: "ta_users",
|
USERS: "ta_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BbRoles = [
|
export const BudibaseRoles = {
|
||||||
{ label: "App User", value: "appUser" },
|
AppUser: "appUser",
|
||||||
{ label: "Developer", value: "developer" },
|
Developer: "developer",
|
||||||
{ label: "Admin", value: "admin" },
|
Admin: "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BudibaseRoleOptions = [
|
||||||
|
{ label: "App User", value: BudibaseRoles.AppUser },
|
||||||
|
{ label: "Developer", value: BudibaseRoles.Developer },
|
||||||
|
{ label: "Admin", value: BudibaseRoles.Admin },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const BuilderRoleDescriptions = [
|
export const BuilderRoleDescriptions = [
|
||||||
{
|
{
|
||||||
value: "appUser",
|
value: BudibaseRoles.AppUser,
|
||||||
icon: "User",
|
icon: "User",
|
||||||
label: "App user - Only has access to published apps",
|
label: "App user - Only has access to published apps",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "developer",
|
value: BudibaseRoles.Developer,
|
||||||
icon: "Hammer",
|
icon: "Hammer",
|
||||||
label: "Developer - Access to the app builder",
|
label: "Developer - Access to the app builder",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "admin",
|
value: BudibaseRoles.Admin,
|
||||||
icon: "Draw",
|
icon: "Draw",
|
||||||
label: "Admin - Full access",
|
label: "Admin - Full access",
|
||||||
},
|
},
|
||||||
|
|
|
@ -170,6 +170,7 @@ export default class DataFetch {
|
||||||
rows: page.rows,
|
rows: page.rows,
|
||||||
info: page.info,
|
info: page.info,
|
||||||
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
||||||
|
error: page.error,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +183,7 @@ export default class DataFetch {
|
||||||
const features = get(this.featureStore)
|
const features = get(this.featureStore)
|
||||||
|
|
||||||
// Get the actual data
|
// Get the actual data
|
||||||
let { rows, info, hasNextPage, cursor } = await this.getData()
|
let { rows, info, hasNextPage, cursor, error } = await this.getData()
|
||||||
|
|
||||||
// If we don't support searching, do a client search
|
// If we don't support searching, do a client search
|
||||||
if (!features.supportsSearch) {
|
if (!features.supportsSearch) {
|
||||||
|
@ -204,6 +205,7 @@ export default class DataFetch {
|
||||||
info,
|
info,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
cursor,
|
cursor,
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,8 +347,14 @@ export default class DataFetch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.store.update($store => ({ ...$store, loading: true }))
|
this.store.update($store => ({ ...$store, loading: true }))
|
||||||
const { rows, info } = await this.getPage()
|
const { rows, info, error } = await this.getPage()
|
||||||
this.store.update($store => ({ ...$store, rows, info, loading: false }))
|
this.store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
rows,
|
||||||
|
info,
|
||||||
|
loading: false,
|
||||||
|
error,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -386,7 +394,7 @@ export default class DataFetch {
|
||||||
cursor: nextCursor,
|
cursor: nextCursor,
|
||||||
pageNumber: $store.pageNumber + 1,
|
pageNumber: $store.pageNumber + 1,
|
||||||
}))
|
}))
|
||||||
const { rows, info, hasNextPage, cursor } = await this.getPage()
|
const { rows, info, hasNextPage, cursor, error } = await this.getPage()
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
this.store.update($store => {
|
this.store.update($store => {
|
||||||
|
@ -400,6 +408,7 @@ export default class DataFetch {
|
||||||
info,
|
info,
|
||||||
cursors,
|
cursors,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -421,7 +430,7 @@ export default class DataFetch {
|
||||||
cursor: prevCursor,
|
cursor: prevCursor,
|
||||||
pageNumber: $store.pageNumber - 1,
|
pageNumber: $store.pageNumber - 1,
|
||||||
}))
|
}))
|
||||||
const { rows, info } = await this.getPage()
|
const { rows, info, error } = await this.getPage()
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
this.store.update($store => {
|
this.store.update($store => {
|
||||||
|
@ -430,6 +439,7 @@ export default class DataFetch {
|
||||||
rows,
|
rows,
|
||||||
info,
|
info,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default class TableFetch extends DataFetch {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -77,11 +77,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "1.1.33-alpha.4",
|
"@budibase/backend-core": "^1.2.12",
|
||||||
"@budibase/client": "1.1.33-alpha.4",
|
"@budibase/client": "^1.2.12",
|
||||||
"@budibase/pro": "1.1.33-alpha.4",
|
"@budibase/pro": "1.2.12",
|
||||||
"@budibase/string-templates": "1.1.33-alpha.4",
|
"@budibase/string-templates": "^1.2.12",
|
||||||
"@budibase/types": "1.1.33-alpha.4",
|
"@budibase/types": "^1.2.12",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -128,25 +128,35 @@ exports.search = async ctx => {
|
||||||
[params.sort]: direction,
|
[params.sort]: direction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rows = await handleRequest(DataSourceOperation.READ, tableId, {
|
try {
|
||||||
filters: query,
|
const rows = await handleRequest(DataSourceOperation.READ, tableId, {
|
||||||
sort,
|
|
||||||
paginate: paginateObj,
|
|
||||||
})
|
|
||||||
let hasNextPage = false
|
|
||||||
if (paginate && rows.length === limit) {
|
|
||||||
const nextRows = await handleRequest(DataSourceOperation.READ, tableId, {
|
|
||||||
filters: query,
|
filters: query,
|
||||||
sort,
|
sort,
|
||||||
paginate: {
|
paginate: paginateObj,
|
||||||
limit: 1,
|
|
||||||
page: bookmark * limit + 1,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
hasNextPage = nextRows.length > 0
|
let hasNextPage = false
|
||||||
|
if (paginate && rows.length === limit) {
|
||||||
|
const nextRows = await handleRequest(DataSourceOperation.READ, tableId, {
|
||||||
|
filters: query,
|
||||||
|
sort,
|
||||||
|
paginate: {
|
||||||
|
limit: 1,
|
||||||
|
page: bookmark * limit + 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
hasNextPage = nextRows.length > 0
|
||||||
|
}
|
||||||
|
// need wrapper object for bookmarks etc when paginating
|
||||||
|
return { rows, hasNextPage, bookmark: bookmark + 1 }
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message && err.message.includes("does not exist")) {
|
||||||
|
throw new Error(
|
||||||
|
`Table updated externally, please re-fetch - ${err.message}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// need wrapper object for bookmarks etc when paginating
|
|
||||||
return { rows, hasNextPage, bookmark: bookmark + 1 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.validate = async () => {
|
exports.validate = async () => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ const Sentry = require("@sentry/node")
|
||||||
const fileSystem = require("./utilities/fileSystem")
|
const fileSystem = require("./utilities/fileSystem")
|
||||||
const bullboard = require("./automations/bullboard")
|
const bullboard = require("./automations/bullboard")
|
||||||
const { logAlert } = require("@budibase/backend-core/logging")
|
const { logAlert } = require("@budibase/backend-core/logging")
|
||||||
|
const { pinoSettings } = require("@budibase/backend-core")
|
||||||
const { Thread } = require("./threads")
|
const { Thread } = require("./threads")
|
||||||
import redis from "./utilities/redis"
|
import redis from "./utilities/redis"
|
||||||
import * as migrations from "./migrations"
|
import * as migrations from "./migrations"
|
||||||
|
@ -35,14 +36,7 @@ app.use(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
app.use(
|
app.use(pino(pinoSettings()))
|
||||||
pino({
|
|
||||||
prettyPrint: {
|
|
||||||
levelFirst: true,
|
|
||||||
},
|
|
||||||
level: env.LOG_LEVEL || "error",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
const plugin = bullboard.init()
|
const plugin = bullboard.init()
|
||||||
|
|
|
@ -224,8 +224,9 @@ function shouldCopySpecialColumn(
|
||||||
FieldTypes.ARRAY,
|
FieldTypes.ARRAY,
|
||||||
FieldTypes.FORMULA,
|
FieldTypes.FORMULA,
|
||||||
]
|
]
|
||||||
|
// column has been deleted, remove
|
||||||
if (column && !fetchedColumn) {
|
if (column && !fetchedColumn) {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
const fetchedIsNumber =
|
const fetchedIsNumber =
|
||||||
!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER
|
!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER
|
||||||
|
|
|
@ -1094,19 +1094,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@1.1.33-alpha.4":
|
"@budibase/backend-core@1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450"
|
||||||
integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw==
|
integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/types" "1.1.33-alpha.4"
|
"@budibase/types" "^1.2.12"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-sdk "2.1030.0"
|
aws-sdk "2.1030.0"
|
||||||
bcrypt "5.0.1"
|
bcrypt "5.0.1"
|
||||||
dotenv "16.0.1"
|
dotenv "16.0.1"
|
||||||
emitter-listener "1.1.2"
|
emitter-listener "1.1.2"
|
||||||
ioredis "4.28.0"
|
ioredis "4.28.0"
|
||||||
joi "17.6.0"
|
|
||||||
jsonwebtoken "8.5.1"
|
jsonwebtoken "8.5.1"
|
||||||
koa-passport "4.1.4"
|
koa-passport "4.1.4"
|
||||||
lodash "4.17.21"
|
lodash "4.17.21"
|
||||||
|
@ -1178,13 +1177,13 @@
|
||||||
svelte-flatpickr "^3.2.3"
|
svelte-flatpickr "^3.2.3"
|
||||||
svelte-portal "^1.0.0"
|
svelte-portal "^1.0.0"
|
||||||
|
|
||||||
"@budibase/pro@1.1.33-alpha.4":
|
"@budibase/pro@1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb"
|
||||||
integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g==
|
integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "1.1.33-alpha.4"
|
"@budibase/backend-core" "1.2.12"
|
||||||
"@budibase/types" "1.1.33-alpha.4"
|
"@budibase/types" "1.2.12"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
@ -1207,10 +1206,10 @@
|
||||||
svelte-apexcharts "^1.0.2"
|
svelte-apexcharts "^1.0.2"
|
||||||
svelte-flatpickr "^3.1.0"
|
svelte-flatpickr "^3.1.0"
|
||||||
|
|
||||||
"@budibase/types@1.1.33-alpha.4":
|
"@budibase/types@1.2.12", "@budibase/types@^1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c"
|
||||||
integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning==
|
integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g==
|
||||||
|
|
||||||
"@bull-board/api@3.7.0":
|
"@bull-board/api@3.7.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/types",
|
"name": "@budibase/types",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase types",
|
"description": "Budibase types",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -46,6 +46,7 @@ export enum MigrationName {
|
||||||
EVENT_APP_BACKFILL = "event_app_backfill",
|
EVENT_APP_BACKFILL = "event_app_backfill",
|
||||||
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
||||||
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
||||||
|
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MigrationDefinition {
|
export interface MigrationDefinition {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.1.33-alpha.4",
|
"version": "1.2.12",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -35,10 +35,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "1.1.33-alpha.4",
|
"@budibase/backend-core": "^1.2.12",
|
||||||
"@budibase/pro": "1.1.33-alpha.4",
|
"@budibase/pro": "1.2.12",
|
||||||
"@budibase/string-templates": "1.1.33-alpha.4",
|
"@budibase/string-templates": "^1.2.12",
|
||||||
"@budibase/types": "1.1.33-alpha.4",
|
"@budibase/types": "^1.2.12",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@sentry/node": "6.17.7",
|
"@sentry/node": "6.17.7",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
|
|
|
@ -3,17 +3,18 @@ import { checkInviteCode } from "../../../utilities/redis"
|
||||||
import { sendEmail } from "../../../utilities/email"
|
import { sendEmail } from "../../../utilities/email"
|
||||||
import { users } from "../../../sdk"
|
import { users } from "../../../sdk"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { User, CloudAccount } from "@budibase/types"
|
import { CloudAccount, User } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
events,
|
|
||||||
errors,
|
|
||||||
accounts,
|
accounts,
|
||||||
users as usersCore,
|
|
||||||
tenancy,
|
|
||||||
cache,
|
cache,
|
||||||
|
errors,
|
||||||
|
events,
|
||||||
|
tenancy,
|
||||||
|
users as usersCore,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { checkAnyUserExists } from "../../../utilities/users"
|
import { checkAnyUserExists } from "../../../utilities/users"
|
||||||
import { groups as groupUtils } from "@budibase/pro"
|
import { groups as groupUtils } from "@budibase/pro"
|
||||||
|
|
||||||
const MAX_USERS_UPLOAD_LIMIT = 1000
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
||||||
|
|
||||||
export const save = async (ctx: any) => {
|
export const save = async (ctx: any) => {
|
||||||
|
@ -117,8 +118,7 @@ export const adminUser = async (ctx: any) => {
|
||||||
export const countByApp = async (ctx: any) => {
|
export const countByApp = async (ctx: any) => {
|
||||||
const appId = ctx.params.appId
|
const appId = ctx.params.appId
|
||||||
try {
|
try {
|
||||||
const response = await users.countUsersByApp(appId)
|
ctx.body = await users.countUsersByApp(appId)
|
||||||
ctx.body = response
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status || 400, err)
|
ctx.throw(err.status || 400, err)
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,9 @@ export const countByApp = async (ctx: any) => {
|
||||||
|
|
||||||
export const destroy = async (ctx: any) => {
|
export const destroy = async (ctx: any) => {
|
||||||
const id = ctx.params.id
|
const id = ctx.params.id
|
||||||
|
if (id === ctx.user._id) {
|
||||||
|
ctx.throw(400, "Unable to delete self.")
|
||||||
|
}
|
||||||
|
|
||||||
await users.destroy(id, ctx.user)
|
await users.destroy(id, ctx.user)
|
||||||
|
|
||||||
|
@ -136,6 +139,10 @@ export const destroy = async (ctx: any) => {
|
||||||
|
|
||||||
export const bulkDelete = async (ctx: any) => {
|
export const bulkDelete = async (ctx: any) => {
|
||||||
const { userIds } = ctx.request.body
|
const { userIds } = ctx.request.body
|
||||||
|
if (userIds?.indexOf(ctx.user._id) !== -1) {
|
||||||
|
ctx.throw(400, "Unable to delete self.")
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let usersResponse = await users.bulkDelete(userIds)
|
let usersResponse = await users.bulkDelete(userIds)
|
||||||
|
|
||||||
|
@ -207,13 +214,13 @@ export const invite = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const inviteMultiple = async (ctx: any) => {
|
export const inviteMultiple = async (ctx: any) => {
|
||||||
let { emails, userInfo } = ctx.request.body
|
let users = ctx.request.body
|
||||||
let existing = false
|
let existing = false
|
||||||
let existingEmail
|
let existingEmail
|
||||||
for (let email of emails) {
|
for (let user of users) {
|
||||||
if (await usersCore.getGlobalUserByEmail(email)) {
|
if (await usersCore.getGlobalUserByEmail(user.email)) {
|
||||||
existing = true
|
existing = true
|
||||||
existingEmail = email
|
existingEmail = user.email
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,17 +228,18 @@ export const inviteMultiple = async (ctx: any) => {
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ctx.throw(400, `${existingEmail} already exists`)
|
ctx.throw(400, `${existingEmail} already exists`)
|
||||||
}
|
}
|
||||||
if (!userInfo) {
|
|
||||||
userInfo = {}
|
|
||||||
}
|
|
||||||
userInfo.tenantId = tenancy.getTenantId()
|
|
||||||
const opts: any = {
|
|
||||||
subject: "{{ company }} platform invitation",
|
|
||||||
info: userInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
for (let i = 0; i < users.length; i++) {
|
||||||
await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts)
|
let userInfo = users[i].userInfo
|
||||||
|
if (!userInfo) {
|
||||||
|
userInfo = {}
|
||||||
|
}
|
||||||
|
userInfo.tenantId = tenancy.getTenantId()
|
||||||
|
const opts: any = {
|
||||||
|
subject: "{{ company }} platform invitation",
|
||||||
|
info: userInfo,
|
||||||
|
}
|
||||||
|
await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
const { migrate, MIGRATIONS } = require("../../../migrations")
|
||||||
|
|
||||||
|
export const runMigrations = async (ctx: any) => {
|
||||||
|
const options = ctx.request.body
|
||||||
|
// don't await as can take a while, just return
|
||||||
|
migrate(options)
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchDefinitions = async (ctx: any) => {
|
||||||
|
ctx.body = MIGRATIONS
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
|
@ -106,7 +106,10 @@ router
|
||||||
if (ctx.publicEndpoint) {
|
if (ctx.publicEndpoint) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
if ((!ctx.isAuthenticated || !ctx.user.budibaseAccess) && !ctx.internal) {
|
if (
|
||||||
|
(!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) &&
|
||||||
|
!ctx.internal
|
||||||
|
) {
|
||||||
ctx.throw(403, "Unauthorized - no public worker access")
|
ctx.throw(403, "Unauthorized - no public worker access")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/global/roles")
|
const controller = require("../../controllers/global/roles")
|
||||||
const { adminOnly } = require("@budibase/backend-core/auth")
|
const { builderOrAdmin } = require("@budibase/backend-core/auth")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/global/roles", adminOnly, controller.fetch)
|
.get("/api/global/roles", builderOrAdmin, controller.fetch)
|
||||||
.get("/api/global/roles/:appId", adminOnly, controller.find)
|
.get("/api/global/roles/:appId", builderOrAdmin, controller.find)
|
||||||
.delete("/api/global/roles/:appId", adminOnly, controller.removeAppRole)
|
.delete("/api/global/roles/:appId", builderOrAdmin, controller.removeAppRole)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/global/self")
|
const controller = require("../../controllers/global/self")
|
||||||
const builderOnly = require("../../../middleware/builderOnly")
|
const { builderOnly } = require("@budibase/backend-core/auth")
|
||||||
const { users } = require("../validation")
|
const { users } = require("../validation")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
|
@ -6,7 +6,7 @@ const Joi = require("joi")
|
||||||
const cloudRestricted = require("../../../middleware/cloudRestricted")
|
const cloudRestricted = require("../../../middleware/cloudRestricted")
|
||||||
const { users } = require("../validation")
|
const { users } = require("../validation")
|
||||||
const selfController = require("../../controllers/global/self")
|
const selfController = require("../../controllers/global/self")
|
||||||
const builderOrAdmin = require("../../../middleware/builderOrAdmin")
|
const { builderOrAdmin } = require("@budibase/backend-core/auth")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -32,10 +32,12 @@ function buildInviteValidation() {
|
||||||
|
|
||||||
function buildInviteMultipleValidation() {
|
function buildInviteMultipleValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.array().required().items(
|
||||||
emails: Joi.array().required(),
|
Joi.object({
|
||||||
userInfo: Joi.object().optional(),
|
email: Joi.string(),
|
||||||
}).required())
|
userInfo: Joi.object().optional(),
|
||||||
|
})
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInviteAcceptValidation() {
|
function buildInviteAcceptValidation() {
|
||||||
|
@ -64,7 +66,7 @@ router
|
||||||
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
||||||
.delete("/api/global/users/:id", adminOnly, controller.destroy)
|
.delete("/api/global/users/:id", adminOnly, controller.destroy)
|
||||||
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
|
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
|
||||||
.get("/api/global/users/count/:appId", adminOnly, controller.countByApp)
|
.get("/api/global/users/count/:appId", builderOrAdmin, controller.countByApp)
|
||||||
.get("/api/global/roles/:appId")
|
.get("/api/global/roles/:appId")
|
||||||
.post(
|
.post(
|
||||||
"/api/global/users/invite",
|
"/api/global/users/invite",
|
||||||
|
@ -79,7 +81,7 @@ router
|
||||||
controller.invite
|
controller.invite
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/global/users/inviteMultiple",
|
"/api/global/users/multi/invite",
|
||||||
adminOnly,
|
adminOnly,
|
||||||
buildInviteMultipleValidation(),
|
buildInviteMultipleValidation(),
|
||||||
controller.inviteMultiple
|
controller.inviteMultiple
|
||||||
|
|
|
@ -12,6 +12,7 @@ const tenantsRoutes = require("./system/tenants")
|
||||||
const statusRoutes = require("./system/status")
|
const statusRoutes = require("./system/status")
|
||||||
const selfRoutes = require("./global/self")
|
const selfRoutes = require("./global/self")
|
||||||
const licenseRoutes = require("./global/license")
|
const licenseRoutes = require("./global/license")
|
||||||
|
const migrationRoutes = require("./system/migrations")
|
||||||
|
|
||||||
let userGroupRoutes = api.groups
|
let userGroupRoutes = api.groups
|
||||||
exports.routes = [
|
exports.routes = [
|
||||||
|
@ -29,4 +30,5 @@ exports.routes = [
|
||||||
selfRoutes,
|
selfRoutes,
|
||||||
licenseRoutes,
|
licenseRoutes,
|
||||||
userGroupRoutes,
|
userGroupRoutes,
|
||||||
|
migrationRoutes,
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as migrationsController from "../../controllers/system/migrations"
|
||||||
|
import { auth } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
router
|
||||||
|
.post(
|
||||||
|
"/api/system/migrations/run",
|
||||||
|
auth.internalApi,
|
||||||
|
migrationsController.runMigrations
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/api/system/migrations/definitions",
|
||||||
|
auth.internalApi,
|
||||||
|
migrationsController.fetchDefinitions
|
||||||
|
)
|
||||||
|
|
||||||
|
export = router
|
|
@ -1,4 +1,4 @@
|
||||||
import joiValidator from "../../../middleware/joi-validator"
|
const { joiValidator } = require("@budibase/backend-core/auth")
|
||||||
import Joi from "joi"
|
import Joi from "joi"
|
||||||
|
|
||||||
let schema: any = {
|
let schema: any = {
|
||||||
|
|
|
@ -18,7 +18,7 @@ const http = require("http")
|
||||||
const api = require("./api")
|
const api = require("./api")
|
||||||
const redis = require("./utilities/redis")
|
const redis = require("./utilities/redis")
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
import { events } from "@budibase/backend-core"
|
import { events, pinoSettings } from "@budibase/backend-core"
|
||||||
|
|
||||||
// this will setup http and https proxies form env variables
|
// this will setup http and https proxies form env variables
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
@ -30,14 +30,7 @@ app.keys = ["secret", "key"]
|
||||||
// set up top level koa middleware
|
// set up top level koa middleware
|
||||||
app.use(koaBody({ multipart: true }))
|
app.use(koaBody({ multipart: true }))
|
||||||
app.use(koaSession(app))
|
app.use(koaSession(app))
|
||||||
app.use(
|
app.use(logger(pinoSettings()))
|
||||||
logger({
|
|
||||||
prettyPrint: {
|
|
||||||
levelFirst: true,
|
|
||||||
},
|
|
||||||
level: env.LOG_LEVEL || "error",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// authentication
|
// authentication
|
||||||
app.use(passport.initialize())
|
app.use(passport.initialize())
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
module.exports = async (ctx, next) => {
|
|
||||||
if (
|
|
||||||
!ctx.internal &&
|
|
||||||
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
|
|
||||||
) {
|
|
||||||
ctx.throw(403, "Admin user only endpoint.")
|
|
||||||
}
|
|
||||||
return next()
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
const Joi = require("joi")
|
|
||||||
|
|
||||||
function validate(schema, property) {
|
|
||||||
// Return a Koa middleware function
|
|
||||||
return (ctx, next) => {
|
|
||||||
if (!schema) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
let params = null
|
|
||||||
if (ctx[property] != null) {
|
|
||||||
params = ctx[property]
|
|
||||||
} else if (ctx.request[property] != null) {
|
|
||||||
params = ctx.request[property]
|
|
||||||
}
|
|
||||||
|
|
||||||
schema = schema.append({
|
|
||||||
createdAt: Joi.any().optional(),
|
|
||||||
updatedAt: Joi.any().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
|
||||||
if (error) {
|
|
||||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.body = schema => {
|
|
||||||
return validate(schema, "body")
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.params = schema => {
|
|
||||||
return validate(schema, "params")
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
import * as sdk from "../../sdk"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date:
|
||||||
|
* Aug 2022
|
||||||
|
*
|
||||||
|
* Description:
|
||||||
|
* Re-sync the global-db users to the global-info db users
|
||||||
|
*/
|
||||||
|
export const run = async (globalDb: any) => {
|
||||||
|
const users = (await sdk.users.allUsers()) as User[]
|
||||||
|
const promises = []
|
||||||
|
for (let user of users) {
|
||||||
|
promises.push(
|
||||||
|
sdk.users.addTenant(user.tenantId, user._id as string, user.email)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { migrations, redis } from "@budibase/backend-core"
|
||||||
|
import { Migration, MigrationOptions, MigrationName } from "@budibase/types"
|
||||||
|
import env from "../environment"
|
||||||
|
|
||||||
|
// migration functions
|
||||||
|
import * as syncUserInfo from "./functions/globalInfoSyncUsers"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the migration function and additional configuration from
|
||||||
|
* the static migration definitions.
|
||||||
|
*/
|
||||||
|
export const buildMigrations = () => {
|
||||||
|
const definitions = migrations.DEFINITIONS
|
||||||
|
const workerMigrations: Migration[] = []
|
||||||
|
|
||||||
|
for (const definition of definitions) {
|
||||||
|
switch (definition.name) {
|
||||||
|
case MigrationName.GLOBAL_INFO_SYNC_USERS: {
|
||||||
|
// only needed in cloud
|
||||||
|
if (!env.SELF_HOSTED) {
|
||||||
|
workerMigrations.push({
|
||||||
|
...definition,
|
||||||
|
fn: syncUserInfo.run,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workerMigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MIGRATIONS = buildMigrations()
|
||||||
|
|
||||||
|
export const migrate = async (options?: MigrationOptions) => {
|
||||||
|
if (env.SELF_HOSTED) {
|
||||||
|
await migrateWithLock(options)
|
||||||
|
} else {
|
||||||
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrateWithLock = async (options?: MigrationOptions) => {
|
||||||
|
// get a new lock client
|
||||||
|
const redlock = await redis.clients.getMigrationsRedlock()
|
||||||
|
// lock for 15 minutes
|
||||||
|
const ttl = 1000 * 60 * 15
|
||||||
|
|
||||||
|
let migrationLock
|
||||||
|
|
||||||
|
// acquire lock
|
||||||
|
try {
|
||||||
|
migrationLock = await redlock.lock("migrations", ttl)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.name === "LockError") {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run migrations
|
||||||
|
try {
|
||||||
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
|
} finally {
|
||||||
|
// release lock
|
||||||
|
try {
|
||||||
|
await migrationLock.unlock()
|
||||||
|
} catch (e) {
|
||||||
|
console.error("unable to release migration lock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -101,12 +101,11 @@ interface SaveUserOpts {
|
||||||
bulkCreate?: boolean
|
bulkCreate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildUser = async (
|
const buildUser = async (
|
||||||
user: any,
|
user: any,
|
||||||
opts: SaveUserOpts = {
|
opts: SaveUserOpts = {
|
||||||
hashPassword: true,
|
hashPassword: true,
|
||||||
requirePassword: true,
|
requirePassword: true,
|
||||||
bulkCreate: false,
|
|
||||||
},
|
},
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
dbUser?: any
|
dbUser?: any
|
||||||
|
@ -185,15 +184,12 @@ export const save = async (
|
||||||
dbUser = await db.get(_id)
|
dbUser = await db.get(_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let builtUser = await buildUser(
|
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
||||||
user,
|
|
||||||
{
|
// make sure we set the _id field for a new user
|
||||||
hashPassword: true,
|
if (!_id) {
|
||||||
requirePassword: user.requirePassword,
|
_id = builtUser._id
|
||||||
},
|
}
|
||||||
tenantId,
|
|
||||||
dbUser
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const putOpts = {
|
const putOpts = {
|
||||||
|
@ -220,7 +216,7 @@ export const save = async (
|
||||||
await addTenant(tenantId, _id, email)
|
await addTenant(tenantId, _id, email)
|
||||||
await cache.user.invalidateUser(response.id)
|
await cache.user.invalidateUser(response.id)
|
||||||
// let server know to sync user
|
// let server know to sync user
|
||||||
await apps.syncUserInApps(builtUser._id)
|
await apps.syncUserInApps(_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
|
@ -293,7 +289,6 @@ export const bulkCreate = async (
|
||||||
{
|
{
|
||||||
hashPassword: true,
|
hashPassword: true,
|
||||||
requirePassword: user.requirePassword,
|
requirePassword: user.requirePassword,
|
||||||
bulkCreate: false,
|
|
||||||
},
|
},
|
||||||
tenantId
|
tenantId
|
||||||
)
|
)
|
||||||
|
@ -305,6 +300,9 @@ export const bulkCreate = async (
|
||||||
|
|
||||||
// Post processing of bulk added users, i.e events and cache operations
|
// Post processing of bulk added users, i.e events and cache operations
|
||||||
for (const user of usersToBulkSave) {
|
for (const user of usersToBulkSave) {
|
||||||
|
// TODO: Refactor to bulk insert users into the info db
|
||||||
|
// instead of relying on looping tenant creation
|
||||||
|
await addTenant(tenantId, user._id, user.email)
|
||||||
await eventHelpers.handleSaveEvents(user, null)
|
await eventHelpers.handleSaveEvents(user, null)
|
||||||
await apps.syncUserInApps(user._id)
|
await apps.syncUserInApps(user._id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,19 +291,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/backend-core@1.1.33-alpha.4":
|
"@budibase/backend-core@1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.1.33-alpha.4.tgz#207ffe45d41535e59ccc21cca9892d1e41818a14"
|
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.2.12.tgz#a09ef7c4b7d1217b65e385199f2d044d1f30f450"
|
||||||
integrity sha512-p8SZkODBF4+BhfIYWIkUtJhR04OjvkmkrVTSFWXv2NTkIbSpaJGTkx9Kao+1Dn4N3H4jU4OBdmScy+C8F5MeSw==
|
integrity sha512-o6tsX9bfpMLfAEfxBI4StC6Lvt1PkJ6b0JYlgp8QiXa0WxZX7np24cOxt2fWrP3ASIMDCrzLn2e3k3eOnzvh8w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/types" "1.1.33-alpha.4"
|
"@budibase/types" "^1.2.12"
|
||||||
"@techpass/passport-openidconnect" "0.3.2"
|
"@techpass/passport-openidconnect" "0.3.2"
|
||||||
aws-sdk "2.1030.0"
|
aws-sdk "2.1030.0"
|
||||||
bcrypt "5.0.1"
|
bcrypt "5.0.1"
|
||||||
dotenv "16.0.1"
|
dotenv "16.0.1"
|
||||||
emitter-listener "1.1.2"
|
emitter-listener "1.1.2"
|
||||||
ioredis "4.28.0"
|
ioredis "4.28.0"
|
||||||
joi "17.6.0"
|
|
||||||
jsonwebtoken "8.5.1"
|
jsonwebtoken "8.5.1"
|
||||||
koa-passport "4.1.4"
|
koa-passport "4.1.4"
|
||||||
lodash "4.17.21"
|
lodash "4.17.21"
|
||||||
|
@ -325,21 +324,21 @@
|
||||||
uuid "8.3.2"
|
uuid "8.3.2"
|
||||||
zlib "1.0.5"
|
zlib "1.0.5"
|
||||||
|
|
||||||
"@budibase/pro@1.1.33-alpha.4":
|
"@budibase/pro@1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.1.33-alpha.4.tgz#d25bc2ca73d11adfdc659e324b1e8de31c17657a"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.2.12.tgz#252f8f1c6730a3395d9d340f2e843052bf0293bb"
|
||||||
integrity sha512-CQ3zVbom4ndzIfUznUSERQ4Bz6ZVuy4HbOYGKKkU/FjoWqrYRK1tqlhmfCNQy8P9rnKURCUf3PMoWVWSOAS24g==
|
integrity sha512-1zhMMVBCX+VX/ILPlSbI7tdsQLcrxf1W29IQD4W55AbevlFnpQs4qNVveIdXFm+GWvCJbdN5I26CXBOftbVUhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/backend-core" "1.1.33-alpha.4"
|
"@budibase/backend-core" "1.2.12"
|
||||||
"@budibase/types" "1.1.33-alpha.4"
|
"@budibase/types" "1.2.12"
|
||||||
"@koa/router" "8.0.8"
|
"@koa/router" "8.0.8"
|
||||||
joi "17.6.0"
|
joi "17.6.0"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
"@budibase/types@1.1.33-alpha.4":
|
"@budibase/types@1.2.12", "@budibase/types@^1.2.12":
|
||||||
version "1.1.33-alpha.4"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.1.33-alpha.4.tgz#a8de79c385280389be8b2cc214185caddf5fe4d3"
|
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.2.12.tgz#c460d1b39116538e47e00178116ad066aa6a6f1c"
|
||||||
integrity sha512-od/gbLgbJnHsVlCvBQkuJf3t/Y9VLUNRYPl3Y4IbNOylpj3rSOKVGF3jANQgkI+pOBt5ni3Xlhc7aOI3qAning==
|
integrity sha512-EeWadHUzeLx4X27Pv6XWlpSXbeLIMvg7r+Q52kYyOZFkmjtdCNFQW5PCu1bYUw9L1Xa64t7fvRLjKiNs2xGX7g==
|
||||||
|
|
||||||
"@cspotcode/source-map-consumer@0.8.0":
|
"@cspotcode/source-map-consumer@0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
|
|
Loading…
Reference in New Issue