Merge pull request #7782 from Budibase/feature/day-pass-pricing
Day pass pricing
This commit is contained in:
commit
060251ab03
|
@ -7,6 +7,7 @@ exports.Cookies = {
|
|||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
|
||||
DatasourceAuth: "budibase:datasourceauth",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
|
||||
import { getGlobalDB } from "../context"
|
||||
import PouchDB from "pouchdb"
|
||||
import { StaticDatabases } from "./constants"
|
||||
import { doWithDB } from "./"
|
||||
|
||||
|
@ -201,13 +202,13 @@ export const queryView = async <T>(
|
|||
try {
|
||||
let response = await db.query<T, T>(`database/${viewName}`, params)
|
||||
const rows = response.rows
|
||||
const docs = rows.map((resp: any) =>
|
||||
params.include_docs ? resp.doc : resp.value
|
||||
)
|
||||
const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
|
||||
|
||||
// if arrayResponse has been requested, always return array regardless of length
|
||||
if (opts?.arrayResponse) {
|
||||
return docs
|
||||
} else {
|
||||
// return the single document if there is only one
|
||||
return docs.length <= 1 ? docs[0] : docs
|
||||
}
|
||||
} catch (err: any) {
|
||||
|
|
|
@ -37,7 +37,7 @@ const env = {
|
|||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||
ACCOUNT_PORTAL_URL:
|
||||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
|
||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, code, type) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BudibaseError,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export class BudibaseError extends Error {
|
||||
code: string
|
||||
type: string
|
||||
|
||||
constructor(message: string, code: string, type: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
const { BudibaseError } = require("./base")
|
||||
|
||||
class GenericError extends BudibaseError {
|
||||
constructor(message, code, type) {
|
||||
super(message, code, type ? type : "generic")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GenericError,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { BudibaseError } from "./base"
|
||||
|
||||
export class GenericError extends BudibaseError {
|
||||
constructor(message: string, code: string, type: string) {
|
||||
super(message, code, type ? type : "generic")
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
const { GenericError } = require("./generic")
|
||||
|
||||
class HTTPError extends GenericError {
|
||||
constructor(message, httpStatus, code = "http", type = "generic") {
|
||||
super(message, code, type)
|
||||
this.status = httpStatus
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HTTPError,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { GenericError } from "./generic"
|
||||
|
||||
export class HTTPError extends GenericError {
|
||||
status: number
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
httpStatus: number,
|
||||
code = "http",
|
||||
type = "generic"
|
||||
) {
|
||||
super(message, code, type)
|
||||
this.status = httpStatus
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
const http = require("./http")
|
||||
const licensing = require("./licensing")
|
||||
import { HTTPError } from "./http"
|
||||
import { UsageLimitError, FeatureDisabledError } from "./licensing"
|
||||
import * as licensing from "./licensing"
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
|
@ -11,7 +12,7 @@ const context = {
|
|||
...licensing.context,
|
||||
}
|
||||
|
||||
const getPublicError = err => {
|
||||
const getPublicError = (err: any) => {
|
||||
let error
|
||||
if (err.code || err.type) {
|
||||
// add generic error information
|
||||
|
@ -32,13 +33,15 @@ const getPublicError = err => {
|
|||
return error
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
const pkg = {
|
||||
codes,
|
||||
types,
|
||||
errors: {
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
FeatureDisabledError: licensing.FeatureDisabledError,
|
||||
HTTPError: http.HTTPError,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
HTTPError,
|
||||
},
|
||||
getPublicError,
|
||||
}
|
||||
|
||||
export = pkg
|
|
@ -1,43 +0,0 @@
|
|||
const { HTTPError } = require("./http")
|
||||
|
||||
const type = "license_error"
|
||||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
FEATURE_DISABLED: "feature_disabled",
|
||||
}
|
||||
|
||||
const context = {
|
||||
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
||||
return {
|
||||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
[codes.FEATURE_DISABLED]: err => {
|
||||
return {
|
||||
featureName: err.featureName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class UsageLimitError extends HTTPError {
|
||||
constructor(message, limitName) {
|
||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureDisabledError extends HTTPError {
|
||||
constructor(message, featureName) {
|
||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||
this.featureName = featureName
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
type,
|
||||
codes,
|
||||
context,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { HTTPError } from "./http"
|
||||
|
||||
export const type = "license_error"
|
||||
|
||||
export const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
FEATURE_DISABLED: "feature_disabled",
|
||||
}
|
||||
|
||||
export const context = {
|
||||
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
|
||||
return {
|
||||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
[codes.FEATURE_DISABLED]: (err: any) => {
|
||||
return {
|
||||
featureName: err.featureName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export class UsageLimitError extends HTTPError {
|
||||
limitName: string
|
||||
|
||||
constructor(message: string, limitName: string) {
|
||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureDisabledError extends HTTPError {
|
||||
featureName: string
|
||||
|
||||
constructor(message: string, featureName: string) {
|
||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||
this.featureName = featureName
|
||||
}
|
||||
}
|
|
@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
|||
|
||||
exports.isEnabled = featureFlag => {
|
||||
const tenantId = tenancy.getTenantId()
|
||||
|
||||
return (
|
||||
TENANT_FEATURE_FLAGS &&
|
||||
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||
)
|
||||
const flags = exports.getTenantFeatureFlags(tenantId)
|
||||
return flags.includes(featureFlag)
|
||||
}
|
||||
|
||||
exports.getTenantFeatureFlags = tenantId => {
|
||||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||
return TENANT_FEATURE_FLAGS[tenantId]
|
||||
const flags = []
|
||||
|
||||
if (TENANT_FEATURE_FLAGS) {
|
||||
const globalFlags = TENANT_FEATURE_FLAGS["*"]
|
||||
const tenantFlags = TENANT_FEATURE_FLAGS[tenantId]
|
||||
|
||||
if (globalFlags) {
|
||||
flags.push(...globalFlags)
|
||||
}
|
||||
if (tenantFlags) {
|
||||
flags.push(...tenantFlags)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
return flags
|
||||
}
|
||||
|
||||
exports.FeatureFlag = {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import errors from "./errors"
|
||||
|
||||
const errorClasses = errors.errors
|
||||
import * as events from "./events"
|
||||
import * as migrations from "./migrations"
|
||||
|
@ -15,7 +14,7 @@ import deprovisioning from "./context/deprovision"
|
|||
import auth from "./auth"
|
||||
import constants from "./constants"
|
||||
import * as dbConstants from "./db/constants"
|
||||
import logging from "./logging"
|
||||
import * as logging from "./logging"
|
||||
import pino from "./pino"
|
||||
import * as middleware from "./middleware"
|
||||
import plugins from "./plugin"
|
||||
|
|
|
@ -106,6 +106,7 @@ export = (
|
|||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
// make sure we denote that the session is still in use
|
||||
await updateSessionTTL(session)
|
||||
|
|
|
@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
|||
type: MigrationType.APP,
|
||||
name: MigrationName.APP_URLS,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.DEVELOPER_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.PUBLISHED_APP_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.APP,
|
||||
name: MigrationName.EVENT_APP_BACKFILL,
|
||||
|
|
|
@ -2,28 +2,12 @@ const redis = require("../redis/init")
|
|||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
const env = require("../environment")
|
||||
|
||||
interface CreateSession {
|
||||
sessionId: string
|
||||
tenantId: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
interface Session extends CreateSession {
|
||||
userId: string
|
||||
lastAccessedAt: string
|
||||
createdAt: string
|
||||
// make optional attributes required
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
interface SessionKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
interface ScannedSession {
|
||||
value: Session
|
||||
}
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
SessionKey,
|
||||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
|
|
@ -1,29 +1,39 @@
|
|||
const {
|
||||
import {
|
||||
ViewName,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
} = require("./db/utils")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { UNICODE_MAX } = require("./db/constants")
|
||||
} from "./db/utils"
|
||||
import { queryGlobalView } from "./db/views"
|
||||
import { UNICODE_MAX } from "./db/constants"
|
||||
import { User } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* Given an email address this will use a view to search through
|
||||
* all the users to find one with this email address.
|
||||
* @param {string} email the email to lookup the user by.
|
||||
*/
|
||||
exports.getGlobalUserByEmail = async email => {
|
||||
export const getGlobalUserByEmail = async (
|
||||
email: String
|
||||
): Promise<User | undefined> => {
|
||||
if (email == null) {
|
||||
throw "Must supply an email address to view"
|
||||
}
|
||||
|
||||
return await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||
key: email.toLowerCase(),
|
||||
include_docs: true,
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
// shouldn't be able to happen, but need to handle just in case
|
||||
throw new Error(`Multiple users found with email address: ${email}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||
export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||
if (typeof appId !== "string") {
|
||||
throw new Error("Must provide a string based app ID")
|
||||
}
|
||||
|
@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
|
|||
return Array.isArray(response) ? response : [response]
|
||||
}
|
||||
|
||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
||||
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
return generateAppUserID(getProdAppID(appId), user._id)
|
||||
return generateAppUserID(getProdAppID(appId), user._id!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a starts with search on the global email view.
|
||||
*/
|
||||
exports.searchGlobalUsersByEmail = async (email, opts) => {
|
||||
export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||
if (typeof email !== "string") {
|
||||
throw new Error("Must provide a string to search by")
|
||||
}
|
||||
const lcEmail = email.toLowerCase()
|
||||
// handle if passing up startkey for pagination
|
||||
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
||||
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||
...opts,
|
||||
startkey,
|
||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
|
|||
return app && app.appId ? app.appId : undefined
|
||||
}
|
||||
|
||||
exports.isServingApp = ctx => {
|
||||
// dev app
|
||||
if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
|
||||
return true
|
||||
}
|
||||
// prod app
|
||||
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request tries to find the appId, which can be located in various places
|
||||
* @param {object} ctx The main request body to look through.
|
||||
|
|
|
@ -4,22 +4,32 @@
|
|||
import { banner } from "../Stores/banner"
|
||||
import Banner from "./Banner.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
</script>
|
||||
|
||||
<Portal target=".banner-container">
|
||||
<div class="banner">
|
||||
{#if $banner.message}
|
||||
{#each $banner.messages as message}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Banner
|
||||
type={$banner.type}
|
||||
extraButtonText={$banner.extraButtonText}
|
||||
extraButtonAction={$banner.extraButtonAction}
|
||||
on:change={$banner.onChange}
|
||||
type={message.type}
|
||||
extraButtonText={message.extraButtonText}
|
||||
extraButtonAction={message.extraButtonAction}
|
||||
on:change={() => {
|
||||
if (message.onChange) {
|
||||
message.onChange()
|
||||
}
|
||||
}}
|
||||
showCloseButton={typeof message.showCloseButton === "boolean"
|
||||
? message.showCloseButton
|
||||
: true}
|
||||
>
|
||||
{$banner.message}
|
||||
<TooltipWrapper tooltip={message.tooltip} disabled={false}>
|
||||
{message.message}
|
||||
</TooltipWrapper>
|
||||
</Banner>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
export let header = ""
|
||||
export let message = ""
|
||||
export let onConfirm = undefined
|
||||
export let buttonText = ""
|
||||
|
||||
$: icon = selectIcon(type)
|
||||
// if newlines used, convert them to different elements
|
||||
|
@ -39,13 +40,16 @@
|
|||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||
{/each}
|
||||
{#if onConfirm}
|
||||
<div class="spectrum-InLineAlert-footer">
|
||||
<Button secondary on:click={onConfirm}>OK</Button>
|
||||
<div class="spectrum-InLineAlert-footer button">
|
||||
<Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.spectrum-InLineAlert {
|
||||
--spectrum-semantic-negative-border-color: #e34850;
|
||||
--spectrum-semantic-positive-border-color: #2d9d78;
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let duration = 1000
|
||||
export let width = false
|
||||
export let sideLabel = false
|
||||
export let hidePercentage = true
|
||||
export let color // red, green, default = blue
|
||||
|
||||
export let size = "M"
|
||||
|
||||
|
@ -37,7 +39,7 @@
|
|||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value || value === 0}
|
||||
{#if !hidePercentage && (value || value === 0)}
|
||||
<div
|
||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||
>
|
||||
|
@ -47,8 +49,19 @@
|
|||
<div class="spectrum-ProgressBar-track">
|
||||
<div
|
||||
class="spectrum-ProgressBar-fill"
|
||||
class:color-green={color === "green"}
|
||||
class:color-red={color === "red"}
|
||||
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||
/>
|
||||
</div>
|
||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.color-green {
|
||||
background: #009562;
|
||||
}
|
||||
.color-red {
|
||||
background: #dd2019;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
const multilevel = getContext("sidenav-type")
|
||||
import Badge from "../Badge/Badge.svelte"
|
||||
export let href = ""
|
||||
export let external = false
|
||||
export let heading = ""
|
||||
|
@ -8,6 +9,7 @@
|
|||
export let selected = false
|
||||
export let disabled = false
|
||||
export let dataCy
|
||||
export let badge = ""
|
||||
</script>
|
||||
|
||||
<li
|
||||
|
@ -38,10 +40,22 @@
|
|||
</svg>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if badge}
|
||||
<div class="badge">
|
||||
<Badge active size="S">{badge}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
{#if multilevel && $$slots.subnav}
|
||||
<ul class="spectrum-SideNav">
|
||||
<slot name="subnav" />
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const BANNER_TYPES = {
|
||||
INFO: "info",
|
||||
NEGATIVE: "negative",
|
||||
}
|
||||
|
||||
export function createBannerStore() {
|
||||
const DEFAULT_CONFIG = {}
|
||||
const DEFAULT_CONFIG = {
|
||||
messages: [],
|
||||
}
|
||||
|
||||
const banner = writable(DEFAULT_CONFIG)
|
||||
|
||||
|
@ -20,17 +27,38 @@ export function createBannerStore() {
|
|||
const showStatus = async () => {
|
||||
const config = {
|
||||
message: "Some systems are experiencing issues",
|
||||
type: "negative",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
extraButtonText: "View Status",
|
||||
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
||||
}
|
||||
|
||||
await show(config)
|
||||
await queue([config])
|
||||
}
|
||||
|
||||
const queue = async entries => {
|
||||
const priority = {
|
||||
[BANNER_TYPES.NEGATIVE]: 0,
|
||||
[BANNER_TYPES.INFO]: 1,
|
||||
}
|
||||
banner.update(store => {
|
||||
const sorted = [...store.messages, ...entries].sort((a, b) => {
|
||||
if (priority[a.type] == priority[b.type]) {
|
||||
return 0
|
||||
}
|
||||
return priority[a.type] < priority[b.type] ? -1 : 1
|
||||
})
|
||||
return {
|
||||
...store,
|
||||
messages: sorted,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: banner.subscribe,
|
||||
showStatus,
|
||||
show,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let tooltip = ""
|
||||
export let size = "M"
|
||||
export let disabled = true
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
@ -19,7 +20,7 @@
|
|||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus
|
||||
>
|
||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||
<Icon name="InfoOutline" size="S" {disabled} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
|
@ -54,7 +55,6 @@
|
|||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
|||
export { default as Page } from "./Layout/Page.svelte"
|
||||
export { default as Link } from "./Link/Link.svelte"
|
||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||
export { default as Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
|
@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
|||
|
||||
// Stores
|
||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||
export { banner } from "./Stores/banner"
|
||||
export { banner, BANNER_TYPES } from "./Stores/banner"
|
||||
|
||||
// Helpers
|
||||
export * as Helpers from "./helpers"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||
import { parse, stringify } from "qs"
|
||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
|
||||
|
||||
const queryHandler = { parse, stringify }
|
||||
</script>
|
||||
|
@ -12,6 +13,9 @@
|
|||
<BannerDisplay />
|
||||
|
||||
<NotificationDisplay />
|
||||
|
||||
<LicensingOverlays />
|
||||
|
||||
<Router {routes} config={{ queryHandler }} />
|
||||
<div class="modal-container" />
|
||||
<HelpIcon />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
|
||||
export const selectedScreen = derived(store, $store => {
|
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const getTemporalStore = () => {
|
||||
const initialValue = {}
|
||||
|
||||
const localStorageKey = `bb-temporal`
|
||||
const store = createLocalStorageStore(localStorageKey, initialValue)
|
||||
|
||||
const setExpiring = (key, data, duration) => {
|
||||
const updated = {
|
||||
...data,
|
||||
expiry: Date.now() + duration * 1000,
|
||||
}
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
[key]: updated,
|
||||
}))
|
||||
}
|
||||
|
||||
const getExpiring = key => {
|
||||
const entry = get(store)[key]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
const currentExpiry = entry.expiry
|
||||
if (currentExpiry < Date.now()) {
|
||||
store.update(state => {
|
||||
delete state[key]
|
||||
return state
|
||||
})
|
||||
return null
|
||||
} else {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { setExpiring, getExpiring },
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let templates
|
||||
|
||||
|
@ -96,15 +97,17 @@
|
|||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{#if $licensing?.usageMetrics?.apps < 100}
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{/if}
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let accountDowngradeModal
|
||||
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
accountDowngradeModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
accountDowngradeModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={accountDowngradeModal} on:show={onShow} on:hide={onDismiss}>
|
||||
<ModalContent
|
||||
title="Your account is now on the Free plan"
|
||||
size="M"
|
||||
showCancelButton={$auth.user.accountPortalAccess}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||
onConfirm={$auth.user.accountPortalAccess
|
||||
? () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
: null}
|
||||
>
|
||||
<Body>
|
||||
The payment for your subscription has failed and we have downgraded your
|
||||
account to the <span class="free-plan">Free plan</span>.
|
||||
</Body>
|
||||
<Body>Upgrade to restore full functionality.</Body>
|
||||
{#if !$auth.user.accountPortalAccess}
|
||||
<Body>Please contact the account holder to upgrade.</Body>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.free-plan {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
|
||||
let appLimitModal
|
||||
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
appLimitModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
appLimitModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={appLimitModal} on:hide={onDismiss}>
|
||||
<ModalContent
|
||||
title="Upgrade to get more apps "
|
||||
size="M"
|
||||
showCancelButton={false}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||
onConfirm={$auth.user.accountPortalAccess
|
||||
? () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
: null}
|
||||
>
|
||||
<Body>
|
||||
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
|
||||
to our Pro plan to get unlimited apps and additional features.
|
||||
</Body>
|
||||
{#if !$auth.user.accountPortalAccess}
|
||||
<Body>Please contact the account holder to upgrade.</Body>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.free-plan {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||
import { licensing, auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let dayPassModal
|
||||
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
|
||||
$: daysRemaining = $licensing.quotaResetDaysRemaining
|
||||
$: quotaResetDate = $licensing.quotaResetDate
|
||||
$: dayPassesUsed = $licensing.usageMetrics?.dayPasses
|
||||
$: dayPassesTitle =
|
||||
dayPassesUsed >= 100
|
||||
? "You have run out of Day Passes"
|
||||
: "You are almost out of Day Passes"
|
||||
$: dayPassesBody =
|
||||
dayPassesUsed >= 100
|
||||
? "Upgrade your account to bring your apps back online."
|
||||
: "Upgrade your account to prevent your apps from going offline."
|
||||
|
||||
export function show() {
|
||||
dayPassModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
dayPassModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={dayPassModal} on:show={onShow} on:hide={onDismiss}>
|
||||
{#if $auth.user.accountPortalAccess}
|
||||
<ModalContent
|
||||
title={dayPassesTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
}}
|
||||
>
|
||||
<Body>
|
||||
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||
</span>
|
||||
</Body>
|
||||
<Body>{dayPassesBody}</Body>
|
||||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent title={dayPassesTitle} size="M" showCancelButton={false}>
|
||||
<Body>
|
||||
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||
</span>
|
||||
</Body>
|
||||
<Body>Please contact your account holder to upgrade.</Body>
|
||||
</ModalContent>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
.tooltip :global(.icon-container) {
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import { licensing, auth } from "stores/portal"
|
||||
import { temporalStore } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import DayPassWarningModal from "./DayPassWarningModal.svelte"
|
||||
import PaymentFailedModal from "./PaymentFailedModal.svelte"
|
||||
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
|
||||
import { ExpiringKeys } from "./constants"
|
||||
import { getBanners } from "./licensingBanners"
|
||||
import { banner } from "@budibase/bbui"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
let queuedBanners = []
|
||||
let queuedModals = []
|
||||
let dayPassModal
|
||||
let paymentFailedModal
|
||||
let accountDowngradeModal
|
||||
let userLoaded = false
|
||||
let loaded = false
|
||||
let licensingLoaded = false
|
||||
let currentModalCfg = null
|
||||
|
||||
const processModals = () => {
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const dismissableModals = [
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL,
|
||||
criteria: () => {
|
||||
return $licensing?.usageMetrics?.dayPasses >= 90
|
||||
},
|
||||
action: () => {
|
||||
dayPassModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
|
||||
criteria: () => {
|
||||
return $licensing.accountPastDue && !$licensing.isFreePlan()
|
||||
},
|
||||
action: () => {
|
||||
paymentFailedModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_PAYMENT_FAILED)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL,
|
||||
criteria: () => {
|
||||
return $licensing?.accountDowngraded
|
||||
},
|
||||
action: () => {
|
||||
accountDowngradeModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL)
|
||||
},
|
||||
},
|
||||
]
|
||||
return dismissableModals.filter(modal => {
|
||||
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
|
||||
})
|
||||
}
|
||||
|
||||
const showNextModal = () => {
|
||||
if (currentModalCfg) {
|
||||
currentModalCfg.cache()
|
||||
}
|
||||
if (queuedModals.length) {
|
||||
currentModalCfg = queuedModals.shift()
|
||||
currentModalCfg.action()
|
||||
} else {
|
||||
currentModalCfg = null
|
||||
}
|
||||
}
|
||||
|
||||
$: if (userLoaded && licensingLoaded && loaded) {
|
||||
queuedModals = processModals()
|
||||
queuedBanners = getBanners()
|
||||
showNextModal()
|
||||
banner.queue(queuedBanners)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
auth.subscribe(state => {
|
||||
if (state.user && !userLoaded) {
|
||||
userLoaded = true
|
||||
}
|
||||
})
|
||||
|
||||
licensing.subscribe(state => {
|
||||
if (state.usageMetrics && !licensingLoaded) {
|
||||
licensingLoaded = true
|
||||
}
|
||||
})
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNextModal} />
|
||||
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNextModal} />
|
||||
<AccountDowngradedModal
|
||||
bind:this={accountDowngradeModal}
|
||||
onDismiss={showNextModal}
|
||||
/>
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||
import { auth, admin, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let paymentFailedModal
|
||||
let pastDueEndDate
|
||||
|
||||
const paymentFailedTitle = "Payment failed"
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
paymentFailedModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
paymentFailedModal.hide()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
licensing.subscribe(state => {
|
||||
pastDueEndDate = state.pastDueEndDate
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:this={paymentFailedModal} on:show={onShow} on:hide={onDismiss}>
|
||||
{#if $auth.user.accountPortalAccess}
|
||||
<ModalContent
|
||||
title={paymentFailedTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
}}
|
||||
>
|
||||
<Body>The payment for your subscription has failed</Body>
|
||||
<Body>
|
||||
Please upgrade your billing details before your account gets downgraded
|
||||
to the free plan
|
||||
</Body>
|
||||
<Body weight={800}>
|
||||
<div class="tooltip-root">
|
||||
{`${$licensing.pastDueDaysRemaining} day${
|
||||
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
|
||||
} remaining`}
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
|
||||
</span>
|
||||
</div>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent title={paymentFailedTitle} size="M" showCancelButton={false}>
|
||||
<Body>The payment for your subscription has failed</Body>
|
||||
<Body>
|
||||
Please upgrade your billing details before your account gets downgraded
|
||||
to the free plan
|
||||
</Body>
|
||||
<Body>Please contact your account holder.</Body>
|
||||
<Body weight={800}>
|
||||
<div class="tooltip-root">
|
||||
{`${$licensing.pastDueDaysRemaining} day${
|
||||
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
|
||||
} remaining`}
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
|
||||
</span>
|
||||
</div>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.tooltip-root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
export const ExpiringKeys = {
|
||||
LICENSING_DAYPASS_WARNING_MODAL: "licensing_daypass_warning_90_modal",
|
||||
LICENSING_DAYPASS_WARNING_BANNER: "licensing_daypass_warning_90_banner",
|
||||
LICENSING_PAYMENT_FAILED: "licensing_payment_failed",
|
||||
LICENSING_ACCOUNT_DOWNGRADED_MODAL: "licensing_account_downgraded_modal",
|
||||
LICENSING_APP_LIMIT_MODAL: "licensing_app_limit_modal",
|
||||
LICENSING_ROWS_WARNING_BANNER: "licensing_rows_warning_banner",
|
||||
LICENSING_AUTOMATIONS_WARNING_BANNER: "licensing_automations_warning_banner",
|
||||
LICENSING_QUERIES_WARNING_BANNER: "licensing_queries_warning_banner",
|
||||
}
|
||||
|
||||
export const StripeStatus = {
|
||||
PAST_DUE: "past_due",
|
||||
ACTIVE: "active",
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
import { ExpiringKeys } from "./constants"
|
||||
import { temporalStore } from "builderStore"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
import { BANNER_TYPES } from "@budibase/bbui"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const defaultAction = key => {
|
||||
if (!get(auth).user.accountPortalAccess) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
extraButtonText: "Upgrade Plan",
|
||||
extraButtonAction: () => {
|
||||
defaultCacheFn(key)
|
||||
window.location.href = `${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const buildUsageInfoBanner = (
|
||||
metricKey,
|
||||
metricLabel,
|
||||
cacheKey,
|
||||
percentageThreshold,
|
||||
customMessage
|
||||
) => {
|
||||
const appAuth = get(auth)
|
||||
const appLicensing = get(licensing)
|
||||
|
||||
const displayPercent =
|
||||
appLicensing?.usageMetrics[metricKey] > 100
|
||||
? 100
|
||||
: appLicensing?.usageMetrics[metricKey]
|
||||
|
||||
let bannerConfig = {
|
||||
key: cacheKey,
|
||||
type: BANNER_TYPES.INFO,
|
||||
onChange: () => {
|
||||
defaultCacheFn(cacheKey)
|
||||
},
|
||||
message: customMessage
|
||||
? customMessage
|
||||
: `You have used ${displayPercent}% of your monthly usage of ${metricLabel} with ${
|
||||
appLicensing.quotaResetDaysRemaining
|
||||
} day${
|
||||
appLicensing.quotaResetDaysRemaining == 1 ? "" : "s"
|
||||
} remaining. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder to upgrade"
|
||||
}`,
|
||||
criteria: () => {
|
||||
return appLicensing?.usageMetrics[metricKey] >= percentageThreshold
|
||||
},
|
||||
tooltip: appLicensing?.quotaResetDate,
|
||||
}
|
||||
|
||||
return !get(auth).user.accountPortalAccess
|
||||
? bannerConfig
|
||||
: {
|
||||
...bannerConfig,
|
||||
...defaultAction(cacheKey),
|
||||
}
|
||||
}
|
||||
|
||||
const buildDayPassBanner = () => {
|
||||
const appAuth = get(auth)
|
||||
const appLicensing = get(licensing)
|
||||
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
|
||||
return {
|
||||
key: "max_dayPasses",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
criteria: () => {
|
||||
return true
|
||||
},
|
||||
message: `Your apps are currently offline. You have exceeded your plans limit for Day Passes. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder to upgrade."
|
||||
}`,
|
||||
...defaultAction(),
|
||||
showCloseButton: false,
|
||||
}
|
||||
}
|
||||
|
||||
return buildUsageInfoBanner(
|
||||
"dayPasses",
|
||||
"Day Passes",
|
||||
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
|
||||
90,
|
||||
`You have used ${
|
||||
appLicensing?.usageMetrics["dayPasses"]
|
||||
}% of your monthly usage of Day Passes with ${
|
||||
appLicensing?.quotaResetDaysRemaining
|
||||
} day${
|
||||
get(licensing).quotaResetDaysRemaining == 1 ? "" : "s"
|
||||
} remaining. All apps will be taken offline if this limit is reached. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder to upgrade."
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
const buildPaymentFailedBanner = () => {
|
||||
return {
|
||||
key: "payment_Failed",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
criteria: () => {
|
||||
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan()
|
||||
},
|
||||
message: `Payment Failed - Please update your billing details or your account will be downgrades in
|
||||
${get(licensing)?.pastDueDaysRemaining} day${
|
||||
get(licensing)?.pastDueDaysRemaining == 1 ? "" : "s"
|
||||
}`,
|
||||
...defaultAction(),
|
||||
showCloseButton: false,
|
||||
tooltip: get(licensing).pastDueEndDate,
|
||||
}
|
||||
}
|
||||
|
||||
export const getBanners = () => {
|
||||
return [
|
||||
buildPaymentFailedBanner(),
|
||||
buildDayPassBanner(ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER),
|
||||
buildUsageInfoBanner(
|
||||
"rows",
|
||||
"Rows",
|
||||
ExpiringKeys.LICENSING_ROWS_WARNING_BANNER,
|
||||
90
|
||||
),
|
||||
buildUsageInfoBanner(
|
||||
"automations",
|
||||
"Automations",
|
||||
ExpiringKeys.LICENSING_AUTOMATIONS_WARNING_BANNER,
|
||||
90
|
||||
),
|
||||
buildUsageInfoBanner(
|
||||
"queries",
|
||||
"Queries",
|
||||
ExpiringKeys.LICENSING_QUERIES_WARNING_BANNER,
|
||||
90
|
||||
),
|
||||
].filter(licensingBanner => {
|
||||
return (
|
||||
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
||||
licensingBanner.criteria()
|
||||
)
|
||||
})
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
<script>
|
||||
import { Body, ProgressBar, Label } from "@budibase/bbui"
|
||||
import { Body, ProgressBar, Heading, Icon, Link } from "@budibase/bbui"
|
||||
import { admin, auth } from "../../stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
export let usage
|
||||
export let warnWhenFull = false
|
||||
|
||||
let percentage
|
||||
let unlimited = false
|
||||
let showWarning = false
|
||||
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
|
||||
const isUnlimited = () => {
|
||||
if (usage.total === -1) {
|
||||
|
@ -14,29 +19,62 @@
|
|||
}
|
||||
|
||||
const getPercentage = () => {
|
||||
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100)
|
||||
return (usage.used / usage.total) * 100
|
||||
}
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
onMount(() => {
|
||||
unlimited = isUnlimited()
|
||||
percentage = getPercentage()
|
||||
if (warnWhenFull && percentage === 100) {
|
||||
showWarning = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="usage">
|
||||
<div class="info">
|
||||
<Label size="XL">{usage.name}</Label>
|
||||
<div class="header-container">
|
||||
{#if showWarning}
|
||||
<Icon name="Alert" />
|
||||
{/if}
|
||||
<div class="heading header-item">
|
||||
<Heading size="XS" weight="light">{usage.name}</Heading>
|
||||
</div>
|
||||
</div>
|
||||
{#if unlimited}
|
||||
<Body size="S">{usage.used}</Body>
|
||||
<Body size="S">{usage.used} / Unlimited</Body>
|
||||
{:else}
|
||||
<Body size="S">{usage.used} / {usage.total}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if unlimited}
|
||||
<Body size="S">Unlimited</Body>
|
||||
<ProgressBar
|
||||
showPercentage={false}
|
||||
width={"100%"}
|
||||
duration={1}
|
||||
value={100}
|
||||
/>
|
||||
{:else}
|
||||
<ProgressBar width={"100%"} duration={1} value={percentage} />
|
||||
<ProgressBar
|
||||
color={showWarning ? "red" : "green"}
|
||||
showPercentage={false}
|
||||
width={"100%"}
|
||||
duration={1}
|
||||
value={percentage}
|
||||
/>
|
||||
{/if}
|
||||
{#if showWarning}
|
||||
<Body size="S">
|
||||
To get more {usage.name.toLowerCase()}
|
||||
{#if accountPortalAccess}
|
||||
<Link href={upgradeUrl}>upgrade your plan</Link>
|
||||
{:else}
|
||||
contact your account holder
|
||||
{/if}
|
||||
</Body>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,6 +89,13 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
}
|
||||
.heading {
|
||||
margin-top: 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<script>
|
||||
import {
|
||||
Detail,
|
||||
Button,
|
||||
Heading,
|
||||
Layout,
|
||||
Body,
|
||||
TooltipWrapper,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let description = ""
|
||||
export let title = ""
|
||||
export let primaryAction
|
||||
export let secondaryAction
|
||||
export let primaryActionText
|
||||
export let secondaryActionText
|
||||
export let primaryCta = true
|
||||
export let textRows = []
|
||||
|
||||
$: primaryDefined = primaryAction && primaryActionText
|
||||
$: secondaryDefined = secondaryAction && secondaryActionText
|
||||
</script>
|
||||
|
||||
<div class="dash-card">
|
||||
<div class="dash-card-header">
|
||||
<div class="header-info">
|
||||
<Layout gap="XS">
|
||||
<div class="dash-card-title">
|
||||
<Detail size="M">{description}</Detail>
|
||||
</div>
|
||||
<Heading size="M">{title}</Heading>
|
||||
{#if textRows.length}
|
||||
<div class="text-rows">
|
||||
{#each textRows as row}
|
||||
{#if row.tooltip}
|
||||
<TooltipWrapper tooltip={row.tooltip}>
|
||||
<Body>{row.message}</Body>
|
||||
</TooltipWrapper>
|
||||
{:else}
|
||||
<Body>{row.message}</Body>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if secondaryDefined}
|
||||
<div>
|
||||
<Button newStyles secondary on:click={secondaryAction}
|
||||
>{secondaryActionText}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if primaryDefined}
|
||||
<div class="primary-button">
|
||||
<Button cta={primaryCta} on:click={primaryAction}
|
||||
>{primaryActionText}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dash-card-body">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dash-card {
|
||||
background: var(--spectrum-alias-background-color-primary);
|
||||
border-radius: var(--border-radius-s);
|
||||
overflow: hidden;
|
||||
min-height: 150px;
|
||||
}
|
||||
.dash-card-header {
|
||||
padding: 15px 25px 20px;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dash-card-body {
|
||||
padding: 25px 30px;
|
||||
}
|
||||
.dash-card-title :global(.spectrum-Detail) {
|
||||
color: var(
|
||||
--spectrum-sidenav-heading-text-color,
|
||||
var(--spectrum-global-color-gray-700)
|
||||
);
|
||||
display: inline-block;
|
||||
}
|
||||
.header-info {
|
||||
flex: 1;
|
||||
}
|
||||
.header-actions {
|
||||
flex: 1;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.header-actions :global(:first-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.text-rows {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
.dash-card-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: flex-start "bul";
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Usage } from "./Usage.svelte"
|
||||
export { default as DashCard } from "./UsageDashCard.svelte"
|
|
@ -58,6 +58,13 @@ export const DefaultAppTheme = {
|
|||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
||||
}
|
||||
|
||||
export const PlanType = {
|
||||
FREE: "free",
|
||||
PRO: "pro",
|
||||
BUSINESS: "business",
|
||||
ENTERPRISE: "enterprise",
|
||||
}
|
||||
|
||||
export const PluginSource = {
|
||||
URL: "URL",
|
||||
NPM: "NPM",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { isActive, redirect, params } from "@roxi/routify"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
@ -63,6 +63,10 @@
|
|||
await auth.getSelf()
|
||||
await admin.init()
|
||||
|
||||
if ($auth.user) {
|
||||
await licensing.init()
|
||||
}
|
||||
|
||||
// Set init info if present
|
||||
if ($params["?template"]) {
|
||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { apps, organisation, auth, groups } from "stores/portal"
|
||||
import { apps, organisation, auth, groups, licensing } from "stores/portal"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { AppStatus } from "constants"
|
||||
import { gradient } from "actions"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import Spaceman from "assets/bb-space-man.svg"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
|
||||
let loaded = false
|
||||
|
@ -91,7 +92,7 @@
|
|||
<div class="content">
|
||||
<Layout noPadding>
|
||||
<div class="header">
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<ActionMenu align="right" dataCy="user-menu">
|
||||
<div slot="control" class="avatar">
|
||||
<Avatar
|
||||
|
@ -131,7 +132,17 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if userApps.length}
|
||||
{#if $licensing.usageMetrics.dayPasses >= 100}
|
||||
<div>
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||
<Heading size="M">
|
||||
{"Your apps are currently offline."}
|
||||
</Heading>
|
||||
Please contact the account holder to get them back online.
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if userApps.length}
|
||||
<Heading>Apps</Heading>
|
||||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
|
@ -194,10 +205,13 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
img.logo {
|
||||
width: 40px;
|
||||
margin-bottom: -12px;
|
||||
}
|
||||
img.spaceman {
|
||||
width: 100px;
|
||||
}
|
||||
.avatar {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
import { admin as adminStore } from "stores/portal"
|
||||
import { organisation, auth, admin as adminStore } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
|
@ -37,14 +36,6 @@
|
|||
href: "/builder/portal/apps",
|
||||
},
|
||||
]
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/settings/usage",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
if (admin) {
|
||||
menu = menu.concat([
|
||||
|
@ -53,9 +44,20 @@
|
|||
href: "/builder/portal/manage/users",
|
||||
heading: "Manage",
|
||||
},
|
||||
isEnabled(FEATURE_FLAGS.USER_GROUPS)
|
||||
? {
|
||||
title: "User Groups",
|
||||
href: "/builder/portal/manage/groups",
|
||||
badge: "New",
|
||||
}
|
||||
: undefined,
|
||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
|
||||
{
|
||||
title: "Plugins",
|
||||
href: "/builder/portal/manage/plugins",
|
||||
badge: "New",
|
||||
},
|
||||
|
||||
{
|
||||
title: "Organisation",
|
||||
|
@ -68,15 +70,6 @@
|
|||
},
|
||||
])
|
||||
|
||||
if (isEnabled(FEATURE_FLAGS.USER_GROUPS)) {
|
||||
let item = {
|
||||
title: "User Groups",
|
||||
href: "/builder/portal/manage/groups",
|
||||
}
|
||||
|
||||
menu.splice(2, 0, item)
|
||||
}
|
||||
|
||||
if (!$adminStore.cloud) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
|
@ -84,13 +77,6 @@
|
|||
href: "/builder/portal/settings/update",
|
||||
},
|
||||
])
|
||||
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
menu = menu.concat({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/settings/upgrade",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu = menu.concat([
|
||||
|
@ -103,14 +89,62 @@
|
|||
}
|
||||
|
||||
// add link to account portal if the user has access
|
||||
if ($auth?.user?.accountPortalAccess) {
|
||||
let accountSectionAdded = false
|
||||
|
||||
// link out to account-portal if account holder in cloud or always in self-host
|
||||
if ($auth?.user?.accountPortalAccess || (!$adminStore.cloud && admin)) {
|
||||
accountSectionAdded = true
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Account",
|
||||
href: $adminStore.accountPortalUrl,
|
||||
heading: "Account",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
// always show usage in self-host or cloud if licensing enabled
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/settings/usage",
|
||||
heading: accountSectionAdded ? "" : "Account",
|
||||
},
|
||||
])
|
||||
|
||||
// show the relevant hosting upgrade page
|
||||
if ($adminStore.cloud && $auth?.user?.accountPortalAccess) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Upgrade",
|
||||
href: $adminStore.accountPortalUrl + "/portal/upgrade",
|
||||
badge: "New",
|
||||
},
|
||||
])
|
||||
} else if (!$adminStore.cloud && admin) {
|
||||
menu = menu.concat({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/settings/upgrade",
|
||||
badge: "New",
|
||||
})
|
||||
}
|
||||
|
||||
// show the billing page to licensed account holders in cloud
|
||||
if (
|
||||
$auth?.user?.accountPortalAccess &&
|
||||
$auth.user.account.stripeCustomerId
|
||||
) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Billing",
|
||||
href: $adminStore.accountPortalUrl + "/portal/billing",
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
menu = menu.filter(item => !!item)
|
||||
return menu
|
||||
}
|
||||
|
||||
|
@ -161,11 +195,12 @@
|
|||
</div>
|
||||
<div class="menu">
|
||||
<Navigation>
|
||||
{#each menu as { title, href, heading }}
|
||||
{#each menu as { title, href, heading, badge }}
|
||||
<Item
|
||||
on:click={hideMobileMenu}
|
||||
selected={$isActive(href)}
|
||||
{href}
|
||||
{badge}
|
||||
{heading}>{title}</Item
|
||||
>
|
||||
{/each}
|
||||
|
|
|
@ -13,12 +13,14 @@
|
|||
} from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { templates } from "stores/portal"
|
||||
import { templates, licensing } from "stores/portal"
|
||||
|
||||
let loaded = $templates?.length
|
||||
let template
|
||||
let creationModal = false
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
|
||||
const welcomeBody =
|
||||
|
@ -29,6 +31,8 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
await templates.load()
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -41,9 +45,13 @@
|
|||
})
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
|
||||
const stopAppCreation = () => {
|
||||
|
@ -52,9 +60,13 @@
|
|||
}
|
||||
|
||||
const initiateAppImport = () => {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -121,6 +133,7 @@
|
|||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.title .welcome > .buttons {
|
||||
|
|
|
@ -15,11 +15,12 @@
|
|||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
import { apps, auth, admin, templates } from "stores/portal"
|
||||
import { apps, auth, admin, templates, licensing } from "stores/portal"
|
||||
import download from "downloadjs"
|
||||
import { goto } from "@roxi/routify"
|
||||
import AppRow from "components/start/AppRow.svelte"
|
||||
|
@ -32,6 +33,7 @@
|
|||
let selectedApp
|
||||
let creationModal
|
||||
let updatingModal
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
let loaded = $apps?.length || $templates?.length
|
||||
let searchTerm = ""
|
||||
|
@ -124,8 +126,10 @@
|
|||
return `${app.name} - Automation error (${errorCount(errors)})`
|
||||
}
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
if ($apps?.length) {
|
||||
const initiateAppCreation = async () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else if ($apps?.length) {
|
||||
$goto("/builder/portal/apps/create")
|
||||
} else {
|
||||
template = null
|
||||
|
@ -225,6 +229,10 @@
|
|||
try {
|
||||
await apps.load()
|
||||
await templates.load()
|
||||
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -405,6 +413,8 @@
|
|||
<UpdateAppModal app={selectedApp} />
|
||||
</Modal>
|
||||
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.appTable {
|
||||
border-top: var(--border-light);
|
||||
|
|
|
@ -38,7 +38,11 @@
|
|||
try {
|
||||
await groups.actions.save(group)
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to save group`)
|
||||
if (error.status === 400) {
|
||||
notifications.error(error.message)
|
||||
} else {
|
||||
notifications.error(`Failed to save group`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,10 +35,14 @@
|
|||
}
|
||||
|
||||
const activate = async () => {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
notifications.success("Successfully activated")
|
||||
try {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
notifications.success("Successfully activated")
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
|
|
|
@ -5,49 +5,74 @@
|
|||
Heading,
|
||||
Layout,
|
||||
notifications,
|
||||
Detail,
|
||||
Link,
|
||||
TooltipWrapper,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import Usage from "components/usage/Usage.svelte"
|
||||
import { admin, auth, licensing } from "../../../../stores/portal"
|
||||
import { PlanType } from "../../../../constants"
|
||||
import { DashCard, Usage } from "../../../../components/usage"
|
||||
|
||||
let staticUsage = []
|
||||
let monthlyUsage = []
|
||||
let cancelAt
|
||||
let loaded = false
|
||||
let textRows = []
|
||||
let daysRemainingInMonth
|
||||
let primaryActionText
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
|
||||
|
||||
const WARN_USAGE = ["Queries", "Automations", "Rows", "Day Passes"]
|
||||
const EXCLUDE_QUOTAS = ["Queries"]
|
||||
|
||||
$: quotaUsage = $licensing.quotaUsage
|
||||
$: license = $auth.user?.license
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
$: quotaReset = quotaUsage?.quotaReset
|
||||
|
||||
const setMonthlyUsage = () => {
|
||||
monthlyUsage = []
|
||||
if (quotaUsage.monthly) {
|
||||
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
|
||||
if (EXCLUDE_QUOTAS.includes(value.name)) {
|
||||
continue
|
||||
}
|
||||
const used = quotaUsage.monthly.current[key]
|
||||
if (used !== undefined) {
|
||||
if (value.value !== 0) {
|
||||
monthlyUsage.push({
|
||||
name: value.name,
|
||||
used: used,
|
||||
used: used ? used : 0,
|
||||
total: value.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
const setStaticUsage = () => {
|
||||
staticUsage = []
|
||||
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
|
||||
if (EXCLUDE_QUOTAS.includes(value.name)) {
|
||||
continue
|
||||
}
|
||||
const used = quotaUsage.usageQuota[key]
|
||||
if (used !== undefined) {
|
||||
if (value.value !== 0) {
|
||||
staticUsage.push({
|
||||
name: value.name,
|
||||
used: used,
|
||||
used: used ? used : 0,
|
||||
total: value.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
staticUsage = staticUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
const setCancelAt = () => {
|
||||
cancelAt = license?.billing?.subscription?.cancelAt
|
||||
}
|
||||
|
||||
const capitalise = string => {
|
||||
|
@ -56,6 +81,70 @@
|
|||
}
|
||||
}
|
||||
|
||||
const planTitle = () => {
|
||||
return capitalise(license?.plan.type)
|
||||
}
|
||||
|
||||
const getDaysRemaining = timestamp => {
|
||||
if (!timestamp) {
|
||||
return
|
||||
}
|
||||
const now = new Date()
|
||||
now.setHours(0)
|
||||
now.setMinutes(0)
|
||||
|
||||
const thenDate = new Date(timestamp)
|
||||
thenDate.setHours(0)
|
||||
thenDate.setMinutes(0)
|
||||
|
||||
const difference = thenDate.getTime() - now
|
||||
// return the difference in days
|
||||
return (difference / (1000 * 3600 * 24)).toFixed(0)
|
||||
}
|
||||
|
||||
const setTextRows = () => {
|
||||
textRows = []
|
||||
|
||||
if (cancelAt) {
|
||||
textRows.push({ message: "Subscription has been cancelled" })
|
||||
textRows.push({
|
||||
message: `${getDaysRemaining(cancelAt * 1000)} days remaining`,
|
||||
tooltip: new Date(cancelAt * 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const setDaysRemainingInMonth = () => {
|
||||
const resetDate = new Date(quotaReset)
|
||||
|
||||
const now = new Date()
|
||||
const difference = resetDate.getTime() - now.getTime()
|
||||
|
||||
// return the difference in days
|
||||
daysRemainingInMonth = (difference / (1000 * 3600 * 24)).toFixed(0)
|
||||
}
|
||||
|
||||
const goToAccountPortal = () => {
|
||||
if (license?.plan.type === PlanType.FREE) {
|
||||
window.location.href = upgradeUrl
|
||||
} else {
|
||||
window.location.href = manageUrl
|
||||
}
|
||||
}
|
||||
|
||||
const setPrimaryActionText = () => {
|
||||
if (license?.plan.type === PlanType.FREE) {
|
||||
primaryActionText = "Upgrade"
|
||||
return
|
||||
}
|
||||
|
||||
if (cancelAt) {
|
||||
primaryActionText = "Renew"
|
||||
} else {
|
||||
primaryActionText = "Manage"
|
||||
}
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
await licensing.getQuotaUsage()
|
||||
|
@ -71,69 +160,98 @@
|
|||
})
|
||||
|
||||
$: {
|
||||
if (license && quotaUsage) {
|
||||
setMonthlyUsage()
|
||||
setStaticUsage()
|
||||
if (license) {
|
||||
setPrimaryActionText()
|
||||
setCancelAt()
|
||||
setTextRows()
|
||||
setDaysRemainingInMonth()
|
||||
|
||||
if (quotaUsage) {
|
||||
setMonthlyUsage()
|
||||
setStaticUsage()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Layout>
|
||||
<Heading>Usage</Heading>
|
||||
<Body
|
||||
>Get information about your current usage within Budibase.
|
||||
{#if $admin.cloud}
|
||||
{#if $auth.user?.accountPortalAccess}
|
||||
<Layout noPadding gap="S">
|
||||
<Heading>Usage</Heading>
|
||||
<Body
|
||||
>Get information about your current usage within Budibase.
|
||||
{#if accountPortalAccess}
|
||||
To upgrade your plan and usage limits visit your <Link
|
||||
size="L"
|
||||
href={upgradeUrl}>Account</Link
|
||||
>.
|
||||
on:click={goToAccountPortal}
|
||||
size="L">Account</Link
|
||||
>
|
||||
{:else}
|
||||
Contact your account holder to upgrade your usage limits.
|
||||
To upgrade your plan and usage limits contact your account holder
|
||||
{/if}
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
<Layout gap="S">
|
||||
<Divider size="S" />
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
<Layout gap="XS">
|
||||
<Body size="S">YOUR PLAN</Body>
|
||||
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
|
||||
</Body>
|
||||
</Layout>
|
||||
<Layout gap="S">
|
||||
<Body size="S">USAGE</Body>
|
||||
<div class="usages">
|
||||
{#each staticUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage {usage} />
|
||||
<Divider />
|
||||
<DashCard
|
||||
description="YOUR CURRENT PLAN"
|
||||
title={planTitle()}
|
||||
{primaryActionText}
|
||||
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
||||
{textRows}
|
||||
>
|
||||
<Layout gap="S" noPadding>
|
||||
<Layout gap="S">
|
||||
<div class="usages">
|
||||
<Layout noPadding>
|
||||
{#each staticUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{#if monthlyUsage.length}
|
||||
<Layout gap="S">
|
||||
<Body size="S">MONTHLY</Body>
|
||||
<div class="usages">
|
||||
{#each monthlyUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage {usage} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{#if monthlyUsage.length}
|
||||
<div class="monthly-container">
|
||||
<Layout gap="S">
|
||||
<Heading size="S" weight="light">Monthly</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
|
||||
>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="usages">
|
||||
<Layout noPadding>
|
||||
{#each monthlyUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
<div />
|
||||
{/if}
|
||||
</DashCard>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.usages {
|
||||
display: grid;
|
||||
column-gap: 60px;
|
||||
row-gap: 50px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.detail :global(.spectrum-Detail) {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.detail :global(.icon) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { auth } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "components/portal/licensing/constants"
|
||||
|
||||
export const createLicensingStore = () => {
|
||||
const DEFAULT = {
|
||||
plans: {},
|
||||
}
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
const store = writable(DEFAULT)
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
await actions.getQuotaUsage()
|
||||
await actions.getUsageMetrics()
|
||||
},
|
||||
getQuotaUsage: async () => {
|
||||
const quotaUsage = await API.getQuotaUsage()
|
||||
store.update(state => {
|
||||
|
@ -18,6 +26,80 @@ export const createLicensingStore = () => {
|
|||
}
|
||||
})
|
||||
},
|
||||
getUsageMetrics: async () => {
|
||||
const quota = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
|
||||
const getMetrics = (keys, license, quota) => {
|
||||
if (!license || !quota || !keys) {
|
||||
return {}
|
||||
}
|
||||
return keys.reduce((acc, key) => {
|
||||
const quotaLimit = license[key].value
|
||||
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
const monthlyMetrics = getMetrics(
|
||||
["dayPasses", "queries", "automations"],
|
||||
license.quotas.usage.monthly,
|
||||
quota.monthly.current
|
||||
)
|
||||
const staticMetrics = getMetrics(
|
||||
["apps", "rows"],
|
||||
license.quotas.usage.static,
|
||||
quota.usageQuota
|
||||
)
|
||||
|
||||
const getDaysBetween = (dateStart, dateEnd) => {
|
||||
return dateEnd > dateStart
|
||||
? Math.round(
|
||||
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
|
||||
)
|
||||
: 0
|
||||
}
|
||||
|
||||
const quotaResetDate = new Date(quota.quotaReset)
|
||||
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
||||
|
||||
const accountDowngraded =
|
||||
license?.billing?.subscription?.downgradeAt &&
|
||||
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
|
||||
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||
license?.plan.type === Constants.PlanType.FREE
|
||||
|
||||
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
|
||||
const downgradeAtMilliseconds =
|
||||
license?.billing?.subscription?.downgradeAt
|
||||
let pastDueDaysRemaining
|
||||
let pastDueEndDate
|
||||
|
||||
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
||||
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
||||
pastDueDaysRemaining = getDaysBetween(
|
||||
new Date(pastDueAtMilliseconds),
|
||||
pastDueEndDate
|
||||
)
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||
quotaResetDaysRemaining,
|
||||
quotaResetDate,
|
||||
accountDowngraded,
|
||||
accountPastDue: pastDueAtMilliseconds != null,
|
||||
pastDueEndDate,
|
||||
pastDueDaysRemaining,
|
||||
isFreePlan: () => {
|
||||
return license?.plan.type === Constants.PlanType.FREE
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<a
|
||||
<script>
|
||||
import { Link } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Link
|
||||
href="https://www.budibase.com/?utm_source=budibase-apps-public-screens&utm_medium=badge&utm_campaign=made-in-budibase"
|
||||
target="_blank"
|
||||
>
|
||||
<div>
|
||||
<img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" />
|
||||
<p>Made In Budibase</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
padding: 10px;
|
||||
|
@ -27,12 +32,7 @@
|
|||
|
||||
p {
|
||||
text-decoration: none;
|
||||
color: var(--spectrum-heading-m-text-color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--spectrum-heading-m-text-color);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
@ -5,9 +5,18 @@
|
|||
import { FieldTypes } from "constants"
|
||||
import active from "svelte-spa-router/active"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import MadeInBudibase from "../MadeInBudibase.svelte"
|
||||
import licensing from "../../licensing"
|
||||
|
||||
const sdk = getContext("sdk")
|
||||
const { routeStore, styleable, linkable, builderStore, currentRole } = sdk
|
||||
const {
|
||||
routeStore,
|
||||
styleable,
|
||||
linkable,
|
||||
builderStore,
|
||||
currentRole,
|
||||
environmentStore,
|
||||
} = sdk
|
||||
const component = getContext("component")
|
||||
const context = getContext("context")
|
||||
|
||||
|
@ -225,6 +234,11 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
|
||||
<MadeInBudibase />
|
||||
{/if}
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="main size--{pageWidthClass}">
|
||||
<slot />
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const PlanType = {
|
||||
FREE: "free",
|
||||
TEAM: "team",
|
||||
BUSINESS: "business",
|
||||
ENTERPRISE: "enterprise",
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { isFreePlan } from "./utils.js"
|
||||
|
||||
export const logoEnabled = () => {
|
||||
return isFreePlan()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import * as features from "./features"
|
||||
|
||||
const licensing = {
|
||||
...features,
|
||||
}
|
||||
|
||||
export default licensing
|
|
@ -0,0 +1,20 @@
|
|||
import { authStore } from "../stores/auth.js"
|
||||
import { get } from "svelte/store"
|
||||
import { PlanType } from "./constants"
|
||||
|
||||
const getLicense = () => {
|
||||
const user = get(authStore)
|
||||
if (user) {
|
||||
return user.license
|
||||
}
|
||||
}
|
||||
|
||||
export const isFreePlan = () => {
|
||||
const license = getLicense()
|
||||
if (license) {
|
||||
return license.plan.type === PlanType.FREE
|
||||
} else {
|
||||
// safety net - no license means free plan
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
rowSelectionStore,
|
||||
componentStore,
|
||||
currentRole,
|
||||
environmentStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -27,6 +28,7 @@ export default {
|
|||
builderStore,
|
||||
uploadStore,
|
||||
componentStore,
|
||||
environmentStore,
|
||||
currentRole,
|
||||
styleable,
|
||||
linkable,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { API } from "api"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
const initialState = {
|
||||
cloud: false,
|
||||
}
|
||||
|
||||
const createEnvironmentStore = () => {
|
||||
const store = writable(initialState)
|
||||
|
||||
const actions = {
|
||||
fetchEnvironment: async () => {
|
||||
try {
|
||||
const environment = await API.getEnvironment()
|
||||
store.set({
|
||||
...initialState,
|
||||
...environment,
|
||||
})
|
||||
} catch (error) {
|
||||
store.set(initialState)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const environmentStore = createEnvironmentStore()
|
|
@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
|
|||
export { componentStore } from "./components"
|
||||
export { uploadStore } from "./uploads.js"
|
||||
export { rowSelectionStore } from "./rowSelection.js"
|
||||
export { environmentStore } from "./environment"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { routeStore } from "./routes"
|
||||
import { appStore } from "./app"
|
||||
import { environmentStore } from "./environment"
|
||||
|
||||
export async function initialise() {
|
||||
await routeStore.actions.fetchRoutes()
|
||||
await appStore.actions.fetchAppDefinition()
|
||||
await environmentStore.actions.fetchEnvironment()
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ export const Cookies = {
|
|||
Auth: "budibase:auth",
|
||||
CurrentApp: "budibase:currentapp",
|
||||
ReturnUrl: "budibase:returnurl",
|
||||
AccountReturnUrl: "budibase:account:returnurl",
|
||||
}
|
||||
|
||||
// Table names
|
||||
|
|
|
@ -17,6 +17,8 @@ const arg = process.argv.slice(2)[0]
|
|||
*/
|
||||
updateDotEnv({
|
||||
ACCOUNT_PORTAL_URL:
|
||||
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
|
||||
arg === "enable"
|
||||
? "http://account.local.com:10001"
|
||||
: "http://localhost:10001",
|
||||
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
||||
}).then(() => console.log("Updated worker!"))
|
||||
|
|
|
@ -472,7 +472,6 @@ const destroyApp = async (ctx: any) => {
|
|||
const result = await db.destroy()
|
||||
|
||||
if (isUnpublish) {
|
||||
await quotas.removePublishedApp()
|
||||
await events.app.unpublished(app)
|
||||
} else {
|
||||
await quotas.removeApp()
|
||||
|
|
|
@ -4,6 +4,15 @@ const { getFullUser } = require("../../utilities/users")
|
|||
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
||||
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
||||
|
||||
/**
|
||||
* Add the attributes that are session based to the current user.
|
||||
*/
|
||||
const addSessionAttributesToUser = ctx => {
|
||||
if (ctx.user) {
|
||||
ctx.body.license = ctx.user.license
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetchSelf = async ctx => {
|
||||
let userId = ctx.user.userId || ctx.user._id
|
||||
/* istanbul ignore next */
|
||||
|
@ -55,4 +64,6 @@ exports.fetchSelf = async ctx => {
|
|||
} else {
|
||||
ctx.body = user
|
||||
}
|
||||
|
||||
addSessionAttributesToUser(ctx)
|
||||
}
|
||||
|
|
|
@ -206,12 +206,7 @@ const _deployApp = async function (ctx: any) {
|
|||
|
||||
console.log("Deploying app...")
|
||||
|
||||
let app
|
||||
if (await isFirstDeploy()) {
|
||||
app = await quotas.addPublishedApp(() => deployApp(deployment))
|
||||
} else {
|
||||
app = await deployApp(deployment)
|
||||
}
|
||||
let app = await deployApp(deployment)
|
||||
|
||||
await events.app.published(app)
|
||||
ctx.body = deployment
|
||||
|
|
|
@ -18,7 +18,6 @@ const { DocumentType } = require("../../../db/utils")
|
|||
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
||||
const { setCookie, clearCookie } = require("@budibase/backend-core/utils")
|
||||
const AWS = require("aws-sdk")
|
||||
|
||||
const fs = require("fs")
|
||||
const {
|
||||
downloadTarballDirect,
|
||||
|
|
|
@ -54,8 +54,8 @@ router
|
|||
noTenancyRequired: true,
|
||||
})
|
||||
)
|
||||
.use(currentApp)
|
||||
.use(pro.licensing())
|
||||
.use(currentApp)
|
||||
.use(auditLog)
|
||||
|
||||
// error handling middleware
|
||||
|
|
|
@ -23,7 +23,6 @@ describe("/users", () => {
|
|||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
|
||||
it("returns a list of users from an instance db", async () => {
|
||||
await config.createUser("uuidx")
|
||||
await config.createUser("uuidy")
|
||||
|
@ -38,7 +37,6 @@ describe("/users", () => {
|
|||
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
|
||||
})
|
||||
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await config.createUser()
|
||||
await checkPermissionsEndpoint({
|
||||
|
|
|
@ -111,12 +111,14 @@ module.exports = async (ctx, next) => {
|
|||
ctx.appId = appId
|
||||
if (roleId) {
|
||||
ctx.roleId = roleId
|
||||
const globalId = ctx.user ? ctx.user._id : undefined
|
||||
const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null
|
||||
ctx.user = {
|
||||
...ctx.user,
|
||||
// override userID with metadata one
|
||||
_id: userId,
|
||||
userId,
|
||||
globalId,
|
||||
roleId,
|
||||
role: await getRole(roleId),
|
||||
}
|
||||
|
|
|
@ -35,12 +35,10 @@ const formatUsage = (usage: QuotaUsage) => {
|
|||
let maxAutomations = 0
|
||||
let maxQueries = 0
|
||||
let rows = 0
|
||||
let developers = 0
|
||||
|
||||
if (usage) {
|
||||
if (usage.usageQuota) {
|
||||
rows = usage.usageQuota.rows
|
||||
developers = usage.usageQuota.developers
|
||||
}
|
||||
|
||||
if (usage.monthly) {
|
||||
|
@ -59,7 +57,6 @@ const formatUsage = (usage: QuotaUsage) => {
|
|||
maxAutomations,
|
||||
maxQueries,
|
||||
rows,
|
||||
developers,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
const { createUserBuildersView } = require("@budibase/backend-core/db")
|
||||
import * as syncDevelopers from "./usageQuotas/syncDevelopers"
|
||||
|
||||
/**
|
||||
* Date:
|
||||
* March 2022
|
||||
*
|
||||
* Description:
|
||||
* Create the builder users view and sync the developer count
|
||||
*/
|
||||
|
||||
export const run = async (db: any) => {
|
||||
await createUserBuildersView(db)
|
||||
await syncDevelopers.run()
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import * as syncPublishedApps from "./usageQuotas/syncPublishedApps"
|
||||
|
||||
/**
|
||||
* Date:
|
||||
* March 2022
|
||||
*
|
||||
* Description:
|
||||
* Sync the published apps count
|
||||
*/
|
||||
|
||||
export const run = async (db: any) => {
|
||||
await syncPublishedApps.run()
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { getTenantId } from "@budibase/backend-core/tenancy"
|
||||
import { utils } from "@budibase/backend-core"
|
||||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
|
||||
|
||||
export const run = async () => {
|
||||
// get developer count
|
||||
const developerCount = await utils.getBuildersCount()
|
||||
|
||||
// sync developer count
|
||||
const tenantId = getTenantId()
|
||||
console.log(
|
||||
`[Tenant: ${tenantId}] Syncing developer count: ${developerCount}`
|
||||
)
|
||||
await quotas.setUsage(
|
||||
developerCount,
|
||||
StaticQuotaName.DEVELOPERS,
|
||||
QuotaUsageType.STATIC
|
||||
)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { getTenantId } from "@budibase/backend-core/tenancy"
|
||||
import { getAllApps } from "@budibase/backend-core/db"
|
||||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
|
||||
|
||||
export const run = async () => {
|
||||
// get app count
|
||||
const opts: any = { dev: false }
|
||||
const prodApps = await getAllApps(opts)
|
||||
const prodAppCount = prodApps ? prodApps.length : 0
|
||||
|
||||
// sync app count
|
||||
const tenantId = getTenantId()
|
||||
console.log(
|
||||
`[Tenant: ${tenantId}] Syncing published app count: ${prodAppCount}`
|
||||
)
|
||||
await quotas.setUsage(
|
||||
prodAppCount,
|
||||
StaticQuotaName.PUBLISHED_APPS,
|
||||
QuotaUsageType.STATIC
|
||||
)
|
||||
}
|
|
@ -6,8 +6,6 @@ import env from "../environment"
|
|||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||
import * as quota1 from "./functions/quotas1"
|
||||
import * as appUrls from "./functions/appUrls"
|
||||
import * as developerQuota from "./functions/developerQuota"
|
||||
import * as publishedAppsQuota from "./functions/publishedAppsQuota"
|
||||
import * as backfill from "./functions/backfill"
|
||||
|
||||
/**
|
||||
|
@ -42,20 +40,6 @@ export const buildMigrations = () => {
|
|||
})
|
||||
break
|
||||
}
|
||||
case MigrationName.DEVELOPER_QUOTA: {
|
||||
serverMigrations.push({
|
||||
...definition,
|
||||
fn: developerQuota.run,
|
||||
})
|
||||
break
|
||||
}
|
||||
case MigrationName.PUBLISHED_APP_QUOTA: {
|
||||
serverMigrations.push({
|
||||
...definition,
|
||||
fn: publishedAppsQuota.run,
|
||||
})
|
||||
break
|
||||
}
|
||||
case MigrationName.EVENT_APP_BACKFILL: {
|
||||
serverMigrations.push({
|
||||
...definition,
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"jest": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "14.18.20",
|
||||
"@types/koa": "2.13.4",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.7.3"
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./user"
|
|
@ -0,0 +1,8 @@
|
|||
export interface PostAccountUserActivity {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface PostAccountUserActivityResponse {
|
||||
userId: string
|
||||
timestamp: number
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from "./account"
|
||||
export * from "./web"
|
||||
|
|
|
@ -22,11 +22,6 @@ export const isCreatePasswordAccount = (
|
|||
account: CreateAccount
|
||||
): account is CreatePassswordAccount => account.authType === AuthType.PASSWORD
|
||||
|
||||
export interface UpdateAccount {
|
||||
stripeCustomerId?: string
|
||||
licenseKey?: string
|
||||
}
|
||||
|
||||
export interface Account extends CreateAccount {
|
||||
// generated
|
||||
accountId: string
|
||||
|
@ -38,6 +33,7 @@ export interface Account extends CreateAccount {
|
|||
tier: string // deprecated
|
||||
stripeCustomerId?: string
|
||||
licenseKey?: string
|
||||
licenseKeyActivatedAt?: number
|
||||
}
|
||||
|
||||
export interface PasswordAccount extends Account {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from "./account"
|
||||
export * from "./user"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export interface CreateAccountUserActivity {
|
||||
accountId: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface AccountUserActivity extends CreateAccountUserActivity {
|
||||
PK: string
|
||||
SK: string
|
||||
}
|
|
@ -16,6 +16,7 @@ export interface User extends Document {
|
|||
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
||||
userGroups?: string[]
|
||||
forceResetPassword?: boolean
|
||||
dayPassRecordedAt?: string
|
||||
}
|
||||
|
||||
export interface UserRoles {
|
||||
|
|
|
@ -3,3 +3,25 @@ export interface AuthToken {
|
|||
tenantId: string
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
export interface CreateSession {
|
||||
sessionId: string
|
||||
tenantId: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
export interface Session extends CreateSession {
|
||||
userId: string
|
||||
lastAccessedAt: string
|
||||
createdAt: string
|
||||
// make optional attributes required
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
export interface SessionKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface ScannedSession {
|
||||
value: Session
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ export * from "./licensing"
|
|||
export * from "./migrations"
|
||||
export * from "./datasources"
|
||||
export * from "./search"
|
||||
export * from "./koa"
|
||||
export * from "./auth"
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Context } from "koa"
|
||||
import { User } from "../documents"
|
||||
import { License } from "../sdk"
|
||||
|
||||
export interface ContextUser extends User {
|
||||
globalId?: string
|
||||
license: License
|
||||
}
|
||||
|
||||
export interface BBContext extends Context {
|
||||
user?: ContextUser
|
||||
body: any
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { PriceDuration } from "./plan"
|
||||
|
||||
export interface Customer {
|
||||
balance: number | null | undefined
|
||||
currency: string | null | undefined
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
amount: number
|
||||
quantity: number
|
||||
duration: PriceDuration
|
||||
cancelAt: number | null | undefined
|
||||
currentPeriodStart: number
|
||||
currentPeriodEnd: number
|
||||
}
|
||||
|
||||
export interface Billing {
|
||||
customer: Customer
|
||||
subscription?: Subscription
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export enum Feature {
|
||||
USER_GROUPS = "userGroups",
|
||||
}
|
|
@ -1 +1,5 @@
|
|||
export * from "./license"
|
||||
export * from "./plan"
|
||||
export * from "./quota"
|
||||
export * from "./feature"
|
||||
export * from "./billing"
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
export interface License {}
|
||||
import { AccountPlan, Quotas, Feature, Billing } from "."
|
||||
|
||||
export interface License {
|
||||
features: Feature[]
|
||||
quotas: Quotas
|
||||
plan: AccountPlan
|
||||
billing?: Billing
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
export interface AccountPlan {
|
||||
type: PlanType
|
||||
price?: Price
|
||||
}
|
||||
|
||||
export enum PlanType {
|
||||
FREE = "free",
|
||||
PRO = "pro",
|
||||
BUSINESS = "business",
|
||||
ENTERPRISE = "enterprise",
|
||||
}
|
||||
|
||||
export enum PriceDuration {
|
||||
MONTHLY = "monthly",
|
||||
YEARLY = "yearly",
|
||||
}
|
||||
|
||||
export interface Price {
|
||||
amount: number
|
||||
amountMonthly: number
|
||||
currency: string
|
||||
duration: PriceDuration
|
||||
priceId: string
|
||||
dayPasses: number
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { PlanType } from "."
|
||||
|
||||
export enum QuotaUsageType {
|
||||
STATIC = "static",
|
||||
MONTHLY = "monthly",
|
||||
}
|
||||
|
||||
export enum QuotaType {
|
||||
USAGE = "usage",
|
||||
CONSTANT = "constant",
|
||||
}
|
||||
|
||||
export enum StaticQuotaName {
|
||||
ROWS = "rows",
|
||||
APPS = "apps",
|
||||
}
|
||||
|
||||
export enum MonthlyQuotaName {
|
||||
QUERIES = "queries",
|
||||
AUTOMATIONS = "automations",
|
||||
DAY_PASSES = "dayPasses",
|
||||
}
|
||||
|
||||
export enum ConstantQuotaName {
|
||||
QUERY_TIMEOUT_SECONDS = "queryTimeoutSeconds",
|
||||
AUTOMATION_LOG_RETENTION_DAYS = "automationLogRetentionDays",
|
||||
}
|
||||
|
||||
export type QuotaName = StaticQuotaName | MonthlyQuotaName | ConstantQuotaName
|
||||
|
||||
export const isStaticQuota = (
|
||||
quotaType: QuotaType,
|
||||
usageType: QuotaUsageType,
|
||||
name: QuotaName
|
||||
): name is StaticQuotaName => {
|
||||
return quotaType === QuotaType.USAGE && usageType === QuotaUsageType.STATIC
|
||||
}
|
||||
|
||||
export const isMonthlyQuota = (
|
||||
quotaType: QuotaType,
|
||||
usageType: QuotaUsageType,
|
||||
name: QuotaName
|
||||
): name is MonthlyQuotaName => {
|
||||
return quotaType === QuotaType.USAGE && usageType === QuotaUsageType.MONTHLY
|
||||
}
|
||||
|
||||
export const isConstantQuota = (
|
||||
quotaType: QuotaType,
|
||||
name: QuotaName
|
||||
): name is ConstantQuotaName => {
|
||||
return quotaType === QuotaType.CONSTANT
|
||||
}
|
||||
|
||||
export type PlanQuotas = {
|
||||
[PlanType.FREE]: Quotas
|
||||
[PlanType.PRO]: Quotas
|
||||
[PlanType.BUSINESS]: Quotas
|
||||
[PlanType.ENTERPRISE]: Quotas
|
||||
}
|
||||
|
||||
export type Quotas = {
|
||||
[QuotaType.USAGE]: {
|
||||
[QuotaUsageType.MONTHLY]: {
|
||||
[MonthlyQuotaName.QUERIES]: Quota
|
||||
[MonthlyQuotaName.AUTOMATIONS]: Quota
|
||||
[MonthlyQuotaName.DAY_PASSES]: Quota
|
||||
}
|
||||
[QuotaUsageType.STATIC]: {
|
||||
[StaticQuotaName.ROWS]: Quota
|
||||
[StaticQuotaName.APPS]: Quota
|
||||
}
|
||||
}
|
||||
[QuotaType.CONSTANT]: {
|
||||
[ConstantQuotaName.QUERY_TIMEOUT_SECONDS]: Quota
|
||||
[ConstantQuotaName.AUTOMATION_LOG_RETENTION_DAYS]: Quota
|
||||
}
|
||||
}
|
||||
|
||||
export interface Quota {
|
||||
name: string
|
||||
value: number
|
||||
}
|
|
@ -41,8 +41,6 @@ export enum MigrationName {
|
|||
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
|
||||
QUOTAS_1 = "quotas_1",
|
||||
APP_URLS = "app_urls",
|
||||
DEVELOPER_QUOTA = "developer_quota",
|
||||
PUBLISHED_APP_QUOTA = "published_apps_quota",
|
||||
EVENT_APP_BACKFILL = "event_app_backfill",
|
||||
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
||||
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
||||
|
|
|
@ -2,11 +2,145 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/accepts@*":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
|
||||
integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/content-disposition@*":
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3"
|
||||
integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==
|
||||
|
||||
"@types/cookies@*":
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81"
|
||||
integrity sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/express" "*"
|
||||
"@types/keygrip" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express-serve-static-core@^4.17.18":
|
||||
version "4.17.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c"
|
||||
integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
|
||||
"@types/express@*":
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
||||
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.18"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/http-assert@*":
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
|
||||
integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==
|
||||
|
||||
"@types/http-errors@*":
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
||||
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
||||
|
||||
"@types/keygrip@*":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||
integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
|
||||
|
||||
"@types/koa-compose@*":
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
|
||||
integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
|
||||
dependencies:
|
||||
"@types/koa" "*"
|
||||
|
||||
"@types/koa@*":
|
||||
version "2.13.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61"
|
||||
integrity sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==
|
||||
dependencies:
|
||||
"@types/accepts" "*"
|
||||
"@types/content-disposition" "*"
|
||||
"@types/cookies" "*"
|
||||
"@types/http-assert" "*"
|
||||
"@types/http-errors" "*"
|
||||
"@types/keygrip" "*"
|
||||
"@types/koa-compose" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/koa@2.13.4":
|
||||
version "2.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
|
||||
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
|
||||
dependencies:
|
||||
"@types/accepts" "*"
|
||||
"@types/content-disposition" "*"
|
||||
"@types/cookies" "*"
|
||||
"@types/http-assert" "*"
|
||||
"@types/http-errors" "*"
|
||||
"@types/keygrip" "*"
|
||||
"@types/koa-compose" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||
|
||||
"@types/node@*":
|
||||
version "18.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7"
|
||||
integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==
|
||||
|
||||
"@types/node@14.18.20":
|
||||
version "14.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
|
||||
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
|
||||
dependencies:
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
|
|
|
@ -17,7 +17,9 @@ const arg = process.argv.slice(2)[0]
|
|||
*/
|
||||
updateDotEnv({
|
||||
ACCOUNT_PORTAL_URL:
|
||||
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
|
||||
arg === "enable"
|
||||
? "http://account.local.com:10001"
|
||||
: "http://localhost:10001",
|
||||
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
||||
PLATFORM_URL:
|
||||
arg === "enable" ? "http://local.com:10000" : "http://localhost:10000",
|
||||
|
|
|
@ -144,6 +144,7 @@ exports.updateSelf = async ctx => {
|
|||
}
|
||||
|
||||
// remove the old password from the user before sending events
|
||||
user._rev = response.rev
|
||||
delete user.password
|
||||
await events.user.updated(user)
|
||||
if (passwordChange) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
jest.mock("nodemailer")
|
||||
import { TestConfiguration, API } from "../../../../tests"
|
||||
import { TestConfiguration, API, mocks } from "../../../../tests"
|
||||
import { events } from "@budibase/backend-core"
|
||||
|
||||
describe("/api/global/self", () => {
|
||||
|
@ -26,6 +26,9 @@ describe("/api/global/self", () => {
|
|||
delete user.password
|
||||
const res = await api.self.updateSelf(user)
|
||||
|
||||
const dbUser = await config.getUser(user.email)
|
||||
user._rev = dbUser._rev
|
||||
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
||||
expect(res.body._id).toBe(user._id)
|
||||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
expect(events.user.updated).toBeCalledWith(user)
|
||||
|
@ -39,6 +42,9 @@ describe("/api/global/self", () => {
|
|||
user.password = "newPassword"
|
||||
const res = await api.self.updateSelf(user)
|
||||
|
||||
const dbUser = await config.getUser(user.email)
|
||||
user._rev = dbUser._rev
|
||||
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
||||
delete user.password
|
||||
expect(res.body._id).toBe(user._id)
|
||||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import env from "../../environment"
|
||||
import { events, accounts, tenancy } from "@budibase/backend-core"
|
||||
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
||||
import { users as pro } from "@budibase/pro"
|
||||
|
||||
export const handleDeleteEvents = async (user: any) => {
|
||||
await events.user.deleted(user)
|
||||
|
@ -51,7 +52,10 @@ const handleAppRoleEvents = async (user: any, existingUser: any) => {
|
|||
await unassignAppRoleEvents(user, roles, existingRoles)
|
||||
}
|
||||
|
||||
export const handleSaveEvents = async (user: any, existingUser: any) => {
|
||||
export const handleSaveEvents = async (
|
||||
user: User,
|
||||
existingUser: User | undefined
|
||||
) => {
|
||||
const tenantId = tenancy.getTenantId()
|
||||
let tenantAccount: CloudAccount | undefined
|
||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||
|
|
|
@ -1,37 +1,35 @@
|
|||
import env from "../../environment"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import * as apps from "../../utilities/appService"
|
||||
import * as eventHelpers from "./events"
|
||||
import {
|
||||
tenancy,
|
||||
utils,
|
||||
db as dbUtils,
|
||||
constants,
|
||||
cache,
|
||||
users as usersCore,
|
||||
deprovisioning,
|
||||
sessions,
|
||||
HTTPError,
|
||||
accounts,
|
||||
migrations,
|
||||
StaticDatabases,
|
||||
ViewName,
|
||||
cache,
|
||||
constants,
|
||||
db as dbUtils,
|
||||
deprovisioning,
|
||||
events,
|
||||
HTTPError,
|
||||
migrations,
|
||||
sessions,
|
||||
tenancy,
|
||||
users as usersCore,
|
||||
utils,
|
||||
ViewName,
|
||||
} from "@budibase/backend-core"
|
||||
import {
|
||||
MigrationType,
|
||||
PlatformUserByEmail,
|
||||
User,
|
||||
BulkCreateUsersResponse,
|
||||
CreateUserResponse,
|
||||
BulkDeleteUsersResponse,
|
||||
CloudAccount,
|
||||
AllDocsResponse,
|
||||
RowResponse,
|
||||
BulkDocsResponse,
|
||||
AccountMetadata,
|
||||
AllDocsResponse,
|
||||
BulkCreateUsersResponse,
|
||||
BulkDeleteUsersResponse,
|
||||
BulkDocsResponse,
|
||||
CloudAccount,
|
||||
CreateUserResponse,
|
||||
InviteUsersRequest,
|
||||
InviteUsersResponse,
|
||||
MigrationType,
|
||||
PlatformUserByEmail,
|
||||
RowResponse,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
import { groups as groupUtils } from "@budibase/pro"
|
||||
import { sendEmail } from "../../utilities/email"
|
||||
|
@ -120,7 +118,7 @@ interface SaveUserOpts {
|
|||
}
|
||||
|
||||
const buildUser = async (
|
||||
user: any,
|
||||
user: User,
|
||||
opts: SaveUserOpts = {
|
||||
hashPassword: true,
|
||||
requirePassword: true,
|
||||
|
@ -230,16 +228,7 @@ export const save = async (
|
|||
|
||||
try {
|
||||
// save the user to db
|
||||
let response
|
||||
const putUserFn = () => {
|
||||
return db.put(builtUser)
|
||||
}
|
||||
|
||||
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
|
||||
response = await quotas.addDeveloper(putUserFn)
|
||||
} else {
|
||||
response = await putUserFn()
|
||||
}
|
||||
let response = await db.put(builtUser)
|
||||
builtUser._rev = response.rev
|
||||
|
||||
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
||||
|
@ -388,13 +377,8 @@ export const bulkCreate = async (
|
|||
newUsers.push(newUser)
|
||||
}
|
||||
|
||||
// Figure out how many builders we are adding and create the promises
|
||||
// array that will be called by bulkDocs
|
||||
let builderCount = 0
|
||||
// create the promises array that will be called by bulkDocs
|
||||
newUsers.forEach((user: any) => {
|
||||
if (eventHelpers.isAddingBuilder(user, null)) {
|
||||
builderCount++
|
||||
}
|
||||
usersToSave.push(
|
||||
buildUser(
|
||||
user,
|
||||
|
@ -408,14 +392,14 @@ export const bulkCreate = async (
|
|||
})
|
||||
|
||||
const usersToBulkSave = await Promise.all(usersToSave)
|
||||
await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
|
||||
await db.bulkDocs(usersToBulkSave)
|
||||
|
||||
// Post processing of bulk added users, i.e events and cache operations
|
||||
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, undefined)
|
||||
await apps.syncUserInApps(user._id)
|
||||
}
|
||||
|
||||
|
@ -475,8 +459,6 @@ export const bulkDelete = async (
|
|||
}
|
||||
|
||||
let groupsToModify: any = {}
|
||||
let builderCount = 0
|
||||
|
||||
// Get users and delete
|
||||
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
||||
include_docs: true,
|
||||
|
@ -497,11 +479,6 @@ export const bulkDelete = async (
|
|||
}
|
||||
}
|
||||
|
||||
// Also figure out how many builders are being deleted
|
||||
if (eventHelpers.isAddingBuilder(user.doc, null)) {
|
||||
builderCount++
|
||||
}
|
||||
|
||||
return user.doc
|
||||
}
|
||||
)
|
||||
|
@ -519,7 +496,6 @@ export const bulkDelete = async (
|
|||
for (let user of usersToDelete) {
|
||||
await bulkDeleteProcessing(user)
|
||||
}
|
||||
await quotas.removeDevelopers(builderCount)
|
||||
|
||||
// Build Response
|
||||
// index users by id
|
||||
|
@ -574,7 +550,6 @@ export const destroy = async (id: string, currentUser: any) => {
|
|||
}
|
||||
|
||||
await eventHelpers.handleDeleteEvents(dbUser)
|
||||
await quotas.removeUser(dbUser)
|
||||
await cache.user.invalidateUser(userId)
|
||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||
// let server know to sync user
|
||||
|
|
|
@ -2,10 +2,9 @@ import {
|
|||
BulkCreateUsersRequest,
|
||||
BulkCreateUsersResponse,
|
||||
BulkDeleteUsersRequest,
|
||||
CreateUserResponse,
|
||||
BulkDeleteUsersResponse,
|
||||
InviteUsersRequest,
|
||||
User,
|
||||
UserDetails,
|
||||
} from "@budibase/types"
|
||||
import TestConfiguration from "../TestConfiguration"
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue