Merge pull request #11305 from Budibase/feature/offline-license-master
Cherry-pick: Offline Licensing
This commit is contained in:
commit
7236fb8141
|
@ -156,6 +156,7 @@ const environment = {
|
|||
: false,
|
||||
VERSION: findVersion(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError {
|
|||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends HTTPError {
|
||||
constructor(message: string) {
|
||||
super(message, 404)
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError extends HTTPError {
|
||||
constructor(message: string) {
|
||||
super(message, 400)
|
||||
}
|
||||
}
|
||||
|
||||
// LICENSING
|
||||
|
||||
export class UsageLimitError extends HTTPError {
|
||||
|
|
|
@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise<string> => {
|
|||
}
|
||||
}
|
||||
|
||||
const getUniqueTenantId = async (tenantId: string): Promise<string> => {
|
||||
export const getUniqueTenantId = async (tenantId: string): Promise<string> => {
|
||||
// make sure this tenantId always matches the tenantId in context
|
||||
return context.doInTenant(tenantId, () => {
|
||||
return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import _ from "lodash"
|
||||
|
||||
export const account = (): Account => {
|
||||
export const account = (partial: Partial<Account> = {}): Account => {
|
||||
return {
|
||||
accountId: uuid(),
|
||||
tenantId: generator.word(),
|
||||
|
@ -29,6 +29,7 @@ export const account = (): Account => {
|
|||
size: "10+",
|
||||
profession: "Software Engineer",
|
||||
quotaUsage: quotas.usage(),
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { structures } from ".."
|
||||
import { generator } from "./generator"
|
||||
import { newid } from "../../../../src/docIds/newid"
|
||||
|
||||
export function id() {
|
||||
|
@ -6,7 +6,7 @@ export function id() {
|
|||
}
|
||||
|
||||
export function rev() {
|
||||
return `${structures.generator.character({
|
||||
return `${generator.character({
|
||||
numeric: true,
|
||||
})}-${structures.uuid().replace(/-/, "")}`
|
||||
})}-${generator.guid().replace(/-/, "")}`
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./platform"
|
|
@ -0,0 +1 @@
|
|||
export * as installation from "./installation"
|
|
@ -0,0 +1,12 @@
|
|||
import { generator } from "../../generator"
|
||||
import { Installation } from "@budibase/types"
|
||||
import * as db from "../../db"
|
||||
|
||||
export function install(): Installation {
|
||||
return {
|
||||
_id: "install",
|
||||
_rev: db.rev(),
|
||||
installId: generator.guid(),
|
||||
version: generator.string(),
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ export * from "./common"
|
|||
export * as accounts from "./accounts"
|
||||
export * as apps from "./apps"
|
||||
export * as db from "./db"
|
||||
export * as docs from "./documents"
|
||||
export * as koa from "./koa"
|
||||
export * as licenses from "./licenses"
|
||||
export * as plugins from "./plugins"
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
Customer,
|
||||
Feature,
|
||||
License,
|
||||
OfflineIdentifier,
|
||||
OfflineLicense,
|
||||
PlanModel,
|
||||
PlanType,
|
||||
PriceDuration,
|
||||
|
@ -11,6 +13,7 @@ import {
|
|||
Quotas,
|
||||
Subscription,
|
||||
} from "@budibase/types"
|
||||
import { generator } from "./generator"
|
||||
|
||||
export function price(): PurchasedPrice {
|
||||
return {
|
||||
|
@ -127,15 +130,15 @@ export function subscription(): Subscription {
|
|||
}
|
||||
}
|
||||
|
||||
export const license = (
|
||||
opts: {
|
||||
quotas?: Quotas
|
||||
plan?: PurchasedPlan
|
||||
planType?: PlanType
|
||||
features?: Feature[]
|
||||
billing?: Billing
|
||||
} = {}
|
||||
): License => {
|
||||
interface GenerateLicenseOpts {
|
||||
quotas?: Quotas
|
||||
plan?: PurchasedPlan
|
||||
planType?: PlanType
|
||||
features?: Feature[]
|
||||
billing?: Billing
|
||||
}
|
||||
|
||||
export const license = (opts: GenerateLicenseOpts = {}): License => {
|
||||
return {
|
||||
features: opts.features || [],
|
||||
quotas: opts.quotas || quotas(),
|
||||
|
@ -143,3 +146,22 @@ export const license = (
|
|||
billing: opts.billing || billing(),
|
||||
}
|
||||
}
|
||||
|
||||
export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense {
|
||||
const base = license(opts)
|
||||
return {
|
||||
...base,
|
||||
expireAt: new Date().toISOString(),
|
||||
identifier: offlineIdentifier(),
|
||||
}
|
||||
}
|
||||
|
||||
export function offlineIdentifier(
|
||||
installId: string = generator.guid(),
|
||||
tenantId: string = generator.guid()
|
||||
): OfflineIdentifier {
|
||||
return {
|
||||
installId,
|
||||
tenantId,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
Label,
|
||||
ButtonGroup,
|
||||
notifications,
|
||||
CopyInput,
|
||||
File,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
@ -21,15 +23,20 @@
|
|||
$: license = $auth.user.license
|
||||
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
// LICENSE KEY
|
||||
|
||||
$: activateDisabled = !licenseKey || licenseKeyDisabled
|
||||
|
||||
let licenseInfo
|
||||
|
||||
let licenseKeyDisabled = false
|
||||
let licenseKeyType = "text"
|
||||
let licenseKey = ""
|
||||
let deleteLicenseKeyModal
|
||||
|
||||
// OFFLINE
|
||||
|
||||
let offlineLicenseIdentifier = ""
|
||||
let offlineLicense = undefined
|
||||
const offlineLicenseExtensions = [".txt"]
|
||||
|
||||
// Make sure page can't be visited directly in cloud
|
||||
$: {
|
||||
if ($admin.cloud) {
|
||||
|
@ -37,28 +44,115 @@
|
|||
}
|
||||
}
|
||||
|
||||
const activate = async () => {
|
||||
// LICENSE KEY
|
||||
|
||||
const getLicenseKey = async () => {
|
||||
try {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
notifications.success("Successfully activated")
|
||||
licenseKey = await API.getLicenseKey()
|
||||
if (licenseKey) {
|
||||
licenseKey = "**********************************************"
|
||||
licenseKeyType = "password"
|
||||
licenseKeyDisabled = true
|
||||
activateDisabled = true
|
||||
}
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
console.error(e)
|
||||
notifications.error("Error retrieving license key")
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = async () => {
|
||||
const activateLicenseKey = async () => {
|
||||
try {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await getLicenseKey()
|
||||
notifications.success("Successfully activated")
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error("Error activating license key")
|
||||
}
|
||||
}
|
||||
|
||||
const deleteLicenseKey = async () => {
|
||||
try {
|
||||
await API.deleteLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
await getLicenseKey()
|
||||
// reset the form
|
||||
licenseKey = ""
|
||||
licenseKeyDisabled = false
|
||||
notifications.success("Successfully deleted")
|
||||
notifications.success("Offline license removed")
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
console.error(e)
|
||||
notifications.error("Error deleting license key")
|
||||
}
|
||||
}
|
||||
|
||||
// OFFLINE LICENSE
|
||||
|
||||
const getOfflineLicense = async () => {
|
||||
try {
|
||||
const license = await API.getOfflineLicense()
|
||||
if (license) {
|
||||
offlineLicense = {
|
||||
name: "license",
|
||||
}
|
||||
} else {
|
||||
offlineLicense = undefined
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error("Error loading offline license")
|
||||
}
|
||||
}
|
||||
|
||||
const getOfflineLicenseIdentifier = async () => {
|
||||
try {
|
||||
const res = await API.getOfflineLicenseIdentifier()
|
||||
offlineLicenseIdentifier = res.identifierBase64
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error("Error loading installation identifier")
|
||||
}
|
||||
}
|
||||
|
||||
async function activateOfflineLicense(offlineLicenseToken) {
|
||||
try {
|
||||
await API.activateOfflineLicense({ offlineLicenseToken })
|
||||
await auth.getSelf()
|
||||
await getOfflineLicense()
|
||||
notifications.success("Successfully activated")
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error("Error activating offline license")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteOfflineLicense() {
|
||||
try {
|
||||
await API.deleteOfflineLicense()
|
||||
await auth.getSelf()
|
||||
await getOfflineLicense()
|
||||
notifications.success("Successfully removed ofline license")
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error("Error upload offline license")
|
||||
}
|
||||
}
|
||||
|
||||
async function onOfflineLicenseChange(event) {
|
||||
if (event.detail) {
|
||||
// prevent file preview jitter by assigning constant
|
||||
// as soon as possible
|
||||
offlineLicense = {
|
||||
name: "license",
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.readAsText(event.detail)
|
||||
reader.onload = () => activateOfflineLicense(reader.result)
|
||||
} else {
|
||||
offlineLicense = undefined
|
||||
await deleteOfflineLicense()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,29 +167,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
// deactivate the license key field if there is a license key set
|
||||
$: {
|
||||
if (licenseInfo?.licenseKey) {
|
||||
licenseKey = "**********************************************"
|
||||
licenseKeyType = "password"
|
||||
licenseKeyDisabled = true
|
||||
activateDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
const setLicenseInfo = async () => {
|
||||
licenseInfo = await API.getLicenseInfo()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await setLicenseInfo()
|
||||
if ($admin.offlineMode) {
|
||||
await Promise.all([getOfflineLicense(), getOfflineLicenseIdentifier()])
|
||||
} else {
|
||||
await getLicenseKey()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $auth.isAdmin}
|
||||
<DeleteLicenseKeyModal
|
||||
bind:this={deleteLicenseKeyModal}
|
||||
onConfirm={destroy}
|
||||
onConfirm={deleteLicenseKey}
|
||||
/>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
|
@ -108,42 +192,82 @@
|
|||
{:else}
|
||||
To manage your plan visit your
|
||||
<Link size="L" href={upgradeUrl}>account</Link>
|
||||
<div> </div>
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Activate</Heading>
|
||||
<Body size="S">Enter your license key below to activate your plan</Body>
|
||||
</Layout>
|
||||
<Layout noPadding>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">License key</Label>
|
||||
<Input
|
||||
thin
|
||||
bind:value={licenseKey}
|
||||
type={licenseKeyType}
|
||||
disabled={licenseKeyDisabled}
|
||||
{#if $admin.offlineMode}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="XS">Installation identifier</Heading>
|
||||
<Body size="S"
|
||||
>Share this with support@budibase.com to obtain your offline license</Body
|
||||
>
|
||||
</Layout>
|
||||
<Layout noPadding>
|
||||
<div class="identifier-input">
|
||||
<CopyInput value={offlineLicenseIdentifier} />
|
||||
</div>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="XS">License</Heading>
|
||||
<Body size="S">Upload your license to activate your plan</Body>
|
||||
</Layout>
|
||||
<Layout noPadding>
|
||||
<div>
|
||||
<File
|
||||
title="Upload license"
|
||||
extensions={offlineLicenseExtensions}
|
||||
value={offlineLicense}
|
||||
on:change={onOfflineLicenseChange}
|
||||
allowClear={true}
|
||||
disabled={!!offlineLicense}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup gap="M">
|
||||
<Button cta on:click={activate} disabled={activateDisabled}>
|
||||
Activate
|
||||
</Button>
|
||||
{#if licenseInfo?.licenseKey}
|
||||
<Button warning on:click={() => deleteLicenseKeyModal.show()}>
|
||||
Delete
|
||||
</Layout>
|
||||
{:else}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="XS">Activate</Heading>
|
||||
<Body size="S">Enter your license key below to activate your plan</Body>
|
||||
</Layout>
|
||||
<Layout noPadding>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">License key</Label>
|
||||
<Input
|
||||
thin
|
||||
bind:value={licenseKey}
|
||||
type={licenseKeyType}
|
||||
disabled={licenseKeyDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup gap="M">
|
||||
<Button cta on:click={activateLicenseKey} disabled={activateDisabled}>
|
||||
Activate
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</Layout>
|
||||
{#if licenseKey}
|
||||
<Button warning on:click={() => deleteLicenseKeyModal.show()}>
|
||||
Delete
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</Layout>
|
||||
{/if}
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Plan</Heading>
|
||||
<Layout noPadding gap="XXS">
|
||||
<Heading size="XS">Plan</Heading>
|
||||
<Layout noPadding gap="S">
|
||||
<Body size="S">You are currently on the {license.plan.type} plan</Body>
|
||||
<div>
|
||||
<Body size="S"
|
||||
>If you purchase or update your plan on the account</Body
|
||||
>
|
||||
<Body size="S"
|
||||
>portal, click the refresh button to sync those changes</Body
|
||||
>
|
||||
</div>
|
||||
<Body size="XS">
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
time:
|
||||
|
@ -169,4 +293,7 @@
|
|||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
.identifier-input {
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = {
|
|||
adminUser: { checked: false },
|
||||
sso: { checked: false },
|
||||
},
|
||||
offlineMode: false,
|
||||
}
|
||||
|
||||
export function createAdminStore() {
|
||||
|
|
|
@ -1,30 +1,58 @@
|
|||
export const buildLicensingEndpoints = API => ({
|
||||
/**
|
||||
* Activates a self hosted license key
|
||||
*/
|
||||
// LICENSE KEY
|
||||
|
||||
activateLicenseKey: async data => {
|
||||
return API.post({
|
||||
url: `/api/global/license/activate`,
|
||||
url: `/api/global/license/key`,
|
||||
body: data,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a self hosted license key
|
||||
*/
|
||||
deleteLicenseKey: async () => {
|
||||
return API.delete({
|
||||
url: `/api/global/license/info`,
|
||||
url: `/api/global/license/key`,
|
||||
})
|
||||
},
|
||||
getLicenseKey: async () => {
|
||||
try {
|
||||
return await API.get({
|
||||
url: "/api/global/license/key",
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the license info - metadata about the license including the
|
||||
* obfuscated license key.
|
||||
*/
|
||||
getLicenseInfo: async () => {
|
||||
return API.get({
|
||||
url: "/api/global/license/info",
|
||||
// OFFLINE LICENSE
|
||||
|
||||
activateOfflineLicense: async ({ offlineLicenseToken }) => {
|
||||
return API.post({
|
||||
url: "/api/global/license/offline",
|
||||
body: {
|
||||
offlineLicenseToken,
|
||||
},
|
||||
})
|
||||
},
|
||||
deleteOfflineLicense: async () => {
|
||||
return API.delete({
|
||||
url: "/api/global/license/offline",
|
||||
})
|
||||
},
|
||||
getOfflineLicense: async () => {
|
||||
try {
|
||||
return await API.get({
|
||||
url: "/api/global/license/offline",
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
},
|
||||
getOfflineLicenseIdentifier: async () => {
|
||||
return await API.get({
|
||||
url: "/api/global/license/offline/identifier",
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -36,7 +64,6 @@ export const buildLicensingEndpoints = API => ({
|
|||
url: "/api/global/license/refresh",
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the usage information for the tenant
|
||||
*/
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 1a5207d91fb9e0835562c708dd9c421973026543
|
||||
Subproject commit 44fa63cc0f536648ac57ba61b62c60dd2812dbc6
|
|
@ -841,7 +841,8 @@
|
|||
"auto",
|
||||
"json",
|
||||
"internal",
|
||||
"barcodeqr"
|
||||
"barcodeqr",
|
||||
"bigint"
|
||||
],
|
||||
"description": "Defines the type of the column, most explain themselves, a link column is a relationship."
|
||||
},
|
||||
|
@ -1045,7 +1046,8 @@
|
|||
"auto",
|
||||
"json",
|
||||
"internal",
|
||||
"barcodeqr"
|
||||
"barcodeqr",
|
||||
"bigint"
|
||||
],
|
||||
"description": "Defines the type of the column, most explain themselves, a link column is a relationship."
|
||||
},
|
||||
|
@ -1260,7 +1262,8 @@
|
|||
"auto",
|
||||
"json",
|
||||
"internal",
|
||||
"barcodeqr"
|
||||
"barcodeqr",
|
||||
"bigint"
|
||||
],
|
||||
"description": "Defines the type of the column, most explain themselves, a link column is a relationship."
|
||||
},
|
||||
|
|
|
@ -768,6 +768,7 @@ components:
|
|||
- json
|
||||
- internal
|
||||
- barcodeqr
|
||||
- bigint
|
||||
description: Defines the type of the column, most explain themselves, a link
|
||||
column is a relationship.
|
||||
constraints:
|
||||
|
@ -931,6 +932,7 @@ components:
|
|||
- json
|
||||
- internal
|
||||
- barcodeqr
|
||||
- bigint
|
||||
description: Defines the type of the column, most explain themselves, a link
|
||||
column is a relationship.
|
||||
constraints:
|
||||
|
@ -1101,6 +1103,7 @@ components:
|
|||
- json
|
||||
- internal
|
||||
- barcodeqr
|
||||
- bigint
|
||||
description: Defines the type of the column, most explain themselves, a link
|
||||
column is a relationship.
|
||||
constraints:
|
||||
|
|
|
@ -81,7 +81,6 @@ const environment = {
|
|||
SELF_HOSTED: process.env.SELF_HOSTED,
|
||||
HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT,
|
||||
FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main",
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
// old
|
||||
CLIENT_ID: process.env.CLIENT_ID,
|
||||
_set(key: string, value: any) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { LicenseOverrides, QuotaUsage } from "../../documents"
|
||||
import { PlanType } from "../../sdk"
|
||||
import { OfflineLicense, PlanType } from "../../sdk"
|
||||
import { ISO8601 } from "../../shared"
|
||||
|
||||
export interface GetLicenseRequest {
|
||||
// All fields should be optional to cater for
|
||||
|
@ -26,3 +27,13 @@ export interface UpdateLicenseRequest {
|
|||
planType?: PlanType
|
||||
overrides?: LicenseOverrides
|
||||
}
|
||||
|
||||
export interface CreateOfflineLicenseRequest {
|
||||
installationIdentifierBase64: string
|
||||
expireAt: ISO8601
|
||||
}
|
||||
|
||||
export interface GetOfflineLicenseResponse {
|
||||
offlineLicenseToken: string
|
||||
license: OfflineLicense
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from "./auditLogs"
|
|||
export * from "./events"
|
||||
export * from "./configs"
|
||||
export * from "./scim"
|
||||
export * from "./license"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// LICENSE KEY
|
||||
|
||||
export interface ActivateLicenseKeyRequest {
|
||||
licenseKey: string
|
||||
}
|
||||
|
||||
export interface GetLicenseKeyResponse {
|
||||
licenseKey: string
|
||||
}
|
||||
|
||||
// OFFLINE LICENSE
|
||||
|
||||
export interface ActivateOfflineLicenseTokenRequest {
|
||||
offlineLicenseToken: string
|
||||
}
|
||||
|
||||
export interface GetOfflineLicenseTokenResponse {
|
||||
offlineLicenseToken: string
|
||||
}
|
||||
|
||||
// IDENTIFIER
|
||||
|
||||
export interface GetOfflineIdentifierResponse {
|
||||
identifierBase64: string
|
||||
}
|
|
@ -51,6 +51,7 @@ export interface Account extends CreateAccount {
|
|||
licenseRequestedAt?: number
|
||||
licenseOverrides?: LicenseOverrides
|
||||
quotaUsage?: QuotaUsage
|
||||
offlineLicenseToken?: string
|
||||
}
|
||||
|
||||
export interface PasswordAccount extends Account {
|
||||
|
|
|
@ -9,6 +9,7 @@ export enum Feature {
|
|||
BRANDING = "branding",
|
||||
SCIM = "scim",
|
||||
SYNC_AUTOMATIONS = "syncAutomations",
|
||||
OFFLINE = "offline",
|
||||
}
|
||||
|
||||
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
import { PurchasedPlan, Quotas, Feature, Billing } from "."
|
||||
import { ISO8601 } from "../../shared"
|
||||
|
||||
export interface OfflineIdentifier {
|
||||
installId: string
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
export interface OfflineLicense extends License {
|
||||
identifier: OfflineIdentifier
|
||||
expireAt: ISO8601
|
||||
}
|
||||
|
||||
export interface License {
|
||||
features: Feature[]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||
}
|
||||
|
||||
export type ISO8601 = string
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
const actual = jest.requireActual("@budibase/pro")
|
||||
const pro = {
|
||||
...actual,
|
||||
licensing: {
|
||||
keys: {
|
||||
activateLicenseKey: jest.fn(),
|
||||
getLicenseKey: jest.fn(),
|
||||
deleteLicenseKey: jest.fn(),
|
||||
},
|
||||
offline: {
|
||||
activateOfflineLicenseToken: jest.fn(),
|
||||
getOfflineLicenseToken: jest.fn(),
|
||||
deleteOfflineLicenseToken: jest.fn(),
|
||||
getIdentifierBase64: jest.fn(),
|
||||
},
|
||||
cache: {
|
||||
...actual.licensing.cache,
|
||||
refresh: jest.fn(),
|
||||
},
|
||||
},
|
||||
quotas: {
|
||||
...actual.quotas,
|
||||
getQuotaUsage: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export = pro
|
|
@ -0,0 +1,133 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Worker API Specification
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: "http://localhost:10000"
|
||||
description: localhost
|
||||
- url: "https://budibaseqa.app"
|
||||
description: QA
|
||||
- url: "https://preprod.qa.budibase.net"
|
||||
description: Preprod
|
||||
- url: "https://budibase.app"
|
||||
description: Production
|
||||
|
||||
tags:
|
||||
- name: license
|
||||
description: License operations
|
||||
|
||||
paths:
|
||||
/api/global/license/key:
|
||||
post:
|
||||
tags:
|
||||
- license
|
||||
summary: Activate license key
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ActivateLicenseKeyRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
get:
|
||||
tags:
|
||||
- license
|
||||
summary: Get license key
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetLicenseKeyResponse'
|
||||
delete:
|
||||
tags:
|
||||
- license
|
||||
summary: Delete license key
|
||||
responses:
|
||||
'204':
|
||||
description: No content
|
||||
/api/global/license/offline:
|
||||
post:
|
||||
tags:
|
||||
- license
|
||||
summary: Activate offline license
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ActivateOfflineLicenseTokenRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
get:
|
||||
tags:
|
||||
- license
|
||||
summary: Get offline license
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetOfflineLicenseTokenResponse'
|
||||
delete:
|
||||
tags:
|
||||
- license
|
||||
summary: Delete offline license
|
||||
responses:
|
||||
'204':
|
||||
description: No content
|
||||
/api/global/license/offline/identifier:
|
||||
get:
|
||||
tags:
|
||||
- license
|
||||
summary: Get offline identifier
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetOfflineIdentifierResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ActivateOfflineLicenseTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
offlineLicenseToken:
|
||||
type: string
|
||||
required:
|
||||
- offlineLicenseToken
|
||||
GetOfflineLicenseTokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
offlineLicenseToken:
|
||||
type: string
|
||||
required:
|
||||
- offlineLicenseToken
|
||||
ActivateLicenseKeyRequest:
|
||||
type: object
|
||||
properties:
|
||||
licenseKey:
|
||||
type: string
|
||||
required:
|
||||
- licenseKey
|
||||
GetLicenseKeyResponse:
|
||||
type: object
|
||||
properties:
|
||||
licenseKey:
|
||||
type: string
|
||||
required:
|
||||
- licenseKey
|
||||
GetOfflineIdentifierResponse:
|
||||
type: object
|
||||
properties:
|
||||
identifierBase64:
|
||||
type: string
|
||||
required:
|
||||
- identifierBase64
|
|
@ -1,34 +1,83 @@
|
|||
import { licensing, quotas } from "@budibase/pro"
|
||||
import {
|
||||
ActivateLicenseKeyRequest,
|
||||
ActivateOfflineLicenseTokenRequest,
|
||||
GetLicenseKeyResponse,
|
||||
GetOfflineIdentifierResponse,
|
||||
GetOfflineLicenseTokenResponse,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const activate = async (ctx: any) => {
|
||||
// LICENSE KEY
|
||||
|
||||
export async function activateLicenseKey(
|
||||
ctx: UserCtx<ActivateLicenseKeyRequest>
|
||||
) {
|
||||
const { licenseKey } = ctx.request.body
|
||||
if (!licenseKey) {
|
||||
ctx.throw(400, "licenseKey is required")
|
||||
}
|
||||
|
||||
await licensing.activateLicenseKey(licenseKey)
|
||||
await licensing.keys.activateLicenseKey(licenseKey)
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
export async function getLicenseKey(ctx: UserCtx<void, GetLicenseKeyResponse>) {
|
||||
const licenseKey = await licensing.keys.getLicenseKey()
|
||||
if (licenseKey) {
|
||||
ctx.body = { licenseKey: "*" }
|
||||
ctx.status = 200
|
||||
} else {
|
||||
ctx.status = 404
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLicenseKey(ctx: UserCtx<void, void>) {
|
||||
await licensing.keys.deleteLicenseKey()
|
||||
ctx.status = 204
|
||||
}
|
||||
|
||||
// OFFLINE LICENSE
|
||||
|
||||
export async function activateOfflineLicenseToken(
|
||||
ctx: UserCtx<ActivateOfflineLicenseTokenRequest>
|
||||
) {
|
||||
const { offlineLicenseToken } = ctx.request.body
|
||||
await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken)
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
export async function getOfflineLicenseToken(
|
||||
ctx: UserCtx<void, GetOfflineLicenseTokenResponse>
|
||||
) {
|
||||
const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken()
|
||||
if (offlineLicenseToken) {
|
||||
ctx.body = { offlineLicenseToken: "*" }
|
||||
ctx.status = 200
|
||||
} else {
|
||||
ctx.status = 404
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteOfflineLicenseToken(ctx: UserCtx<void, void>) {
|
||||
await licensing.offline.deleteOfflineLicenseToken()
|
||||
ctx.status = 204
|
||||
}
|
||||
|
||||
export async function getOfflineLicenseIdentifier(
|
||||
ctx: UserCtx<void, GetOfflineIdentifierResponse>
|
||||
) {
|
||||
const identifierBase64 = await licensing.offline.getIdentifierBase64()
|
||||
ctx.body = { identifierBase64 }
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
// LICENSES
|
||||
|
||||
export const refresh = async (ctx: any) => {
|
||||
await licensing.cache.refresh()
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
export const getInfo = async (ctx: any) => {
|
||||
const licenseInfo = await licensing.getLicenseInfo()
|
||||
if (licenseInfo) {
|
||||
licenseInfo.licenseKey = "*"
|
||||
ctx.body = licenseInfo
|
||||
}
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
export const deleteInfo = async (ctx: any) => {
|
||||
await licensing.deleteLicenseInfo()
|
||||
ctx.status = 200
|
||||
}
|
||||
// USAGE
|
||||
|
||||
export const getQuotaUsage = async (ctx: any) => {
|
||||
ctx.body = await quotas.getQuotaUsage()
|
||||
ctx.status = 200
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Ctx } from "@budibase/types"
|
||||
import env from "../../../environment"
|
||||
import { env as coreEnv } from "@budibase/backend-core"
|
||||
|
||||
export const fetch = async (ctx: Ctx) => {
|
||||
ctx.body = {
|
||||
multiTenancy: !!env.MULTI_TENANCY,
|
||||
offlineMode: !!env.OFFLINE_MODE,
|
||||
offlineMode: !!coreEnv.OFFLINE_MODE,
|
||||
cloud: !env.SELF_HOSTED,
|
||||
accountPortalUrl: env.ACCOUNT_PORTAL_URL,
|
||||
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
|
||||
|
|
|
@ -1,13 +1,44 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../../controllers/global/license"
|
||||
import { middleware } from "@budibase/backend-core"
|
||||
import Joi from "joi"
|
||||
|
||||
const activateLicenseKeyValidator = middleware.joiValidator.body(
|
||||
Joi.object({
|
||||
licenseKey: Joi.string().required(),
|
||||
}).required()
|
||||
)
|
||||
|
||||
const activateOfflineLicenseValidator = middleware.joiValidator.body(
|
||||
Joi.object({
|
||||
offlineLicenseToken: Joi.string().required(),
|
||||
}).required()
|
||||
)
|
||||
|
||||
const router: Router = new Router()
|
||||
|
||||
router
|
||||
.post("/api/global/license/activate", controller.activate)
|
||||
.post("/api/global/license/refresh", controller.refresh)
|
||||
.get("/api/global/license/info", controller.getInfo)
|
||||
.delete("/api/global/license/info", controller.deleteInfo)
|
||||
.get("/api/global/license/usage", controller.getQuotaUsage)
|
||||
// LICENSE KEY
|
||||
.post(
|
||||
"/api/global/license/key",
|
||||
activateLicenseKeyValidator,
|
||||
controller.activateLicenseKey
|
||||
)
|
||||
.get("/api/global/license/key", controller.getLicenseKey)
|
||||
.delete("/api/global/license/key", controller.deleteLicenseKey)
|
||||
// OFFLINE LICENSE
|
||||
.post(
|
||||
"/api/global/license/offline",
|
||||
activateOfflineLicenseValidator,
|
||||
controller.activateOfflineLicenseToken
|
||||
)
|
||||
.get("/api/global/license/offline", controller.getOfflineLicenseToken)
|
||||
.delete("/api/global/license/offline", controller.deleteOfflineLicenseToken)
|
||||
.get(
|
||||
"/api/global/license/offline/identifier",
|
||||
controller.getOfflineLicenseIdentifier
|
||||
)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { TestConfiguration } from "../../../../tests"
|
||||
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
||||
const licensing = mocks.pro.licensing
|
||||
const quotas = mocks.pro.quotas
|
||||
|
||||
describe("/api/global/license", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
@ -12,18 +14,105 @@ describe("/api/global/license", () => {
|
|||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
describe("POST /api/global/license/activate", () => {
|
||||
it("activates license", () => {})
|
||||
describe("POST /api/global/license/refresh", () => {
|
||||
it("returns 200", async () => {
|
||||
const res = await config.api.license.refresh()
|
||||
expect(res.status).toBe(200)
|
||||
expect(licensing.cache.refresh).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /api/global/license/refresh", () => {})
|
||||
describe("GET /api/global/license/usage", () => {
|
||||
it("returns 200 + usage", async () => {
|
||||
const usage = structures.quotas.usage()
|
||||
quotas.getQuotaUsage.mockResolvedValue(usage)
|
||||
const res = await config.api.license.getUsage()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual(usage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /api/global/license/info", () => {})
|
||||
describe("POST /api/global/license/key", () => {
|
||||
it("returns 200", async () => {
|
||||
const res = await config.api.license.activateLicenseKey({
|
||||
licenseKey: "licenseKey",
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey")
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /api/global/license/info", () => {})
|
||||
describe("GET /api/global/license/key", () => {
|
||||
it("returns 404 when not found", async () => {
|
||||
const res = await config.api.license.getLicenseKey()
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
it("returns 200 + license key", async () => {
|
||||
licensing.keys.getLicenseKey.mockResolvedValue("licenseKey")
|
||||
const res = await config.api.license.getLicenseKey()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual({
|
||||
licenseKey: "*",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /api/global/license/usage", () => {})
|
||||
describe("DELETE /api/global/license/key", () => {
|
||||
it("returns 204", async () => {
|
||||
const res = await config.api.license.deleteLicenseKey()
|
||||
expect(licensing.keys.deleteLicenseKey).toBeCalledTimes(1)
|
||||
expect(res.status).toBe(204)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /api/global/license/offline", () => {
|
||||
it("activates offline license", async () => {
|
||||
const res = await config.api.license.activateOfflineLicense({
|
||||
offlineLicenseToken: "offlineLicenseToken",
|
||||
})
|
||||
expect(licensing.offline.activateOfflineLicenseToken).toBeCalledWith(
|
||||
"offlineLicenseToken"
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /api/global/license/offline", () => {
|
||||
it("returns 404 when not found", async () => {
|
||||
const res = await config.api.license.getOfflineLicense()
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
it("returns 200 + offline license token", async () => {
|
||||
licensing.offline.getOfflineLicenseToken.mockResolvedValue(
|
||||
"offlineLicenseToken"
|
||||
)
|
||||
const res = await config.api.license.getOfflineLicense()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual({
|
||||
offlineLicenseToken: "*",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /api/global/license/offline", () => {
|
||||
it("returns 204", async () => {
|
||||
const res = await config.api.license.deleteOfflineLicense()
|
||||
expect(res.status).toBe(204)
|
||||
expect(licensing.offline.deleteOfflineLicenseToken).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /api/global/license/offline/identifier", () => {
|
||||
it("returns 200 + identifier base64", async () => {
|
||||
licensing.offline.getIdentifierBase64.mockResolvedValue("base64")
|
||||
const res = await config.api.license.getOfflineLicenseIdentifier()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual({
|
||||
identifierBase64: "base64",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -61,7 +61,6 @@ const environment = {
|
|||
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
||||
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
/**
|
||||
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
||||
*/
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import mocks from "./mocks"
|
||||
|
||||
// init the licensing mock
|
||||
import * as pro from "@budibase/pro"
|
||||
mocks.licenses.init(pro)
|
||||
mocks.licenses.init(mocks.pro)
|
||||
|
||||
// use unlimited license by default
|
||||
mocks.licenses.useUnlimited()
|
||||
|
@ -238,21 +237,21 @@ class TestConfiguration {
|
|||
|
||||
const db = context.getGlobalDB()
|
||||
|
||||
const id = dbCore.generateDevInfoID(this.user._id)
|
||||
const id = dbCore.generateDevInfoID(this.user!._id)
|
||||
// TODO: dry
|
||||
this.apiKey = encryption.encrypt(
|
||||
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`
|
||||
)
|
||||
const devInfo = {
|
||||
_id: id,
|
||||
userId: this.user._id,
|
||||
userId: this.user!._id,
|
||||
apiKey: this.apiKey,
|
||||
}
|
||||
await db.put(devInfo)
|
||||
})
|
||||
}
|
||||
|
||||
async getUser(email: string): Promise<User> {
|
||||
async getUser(email: string): Promise<User | undefined> {
|
||||
return context.doInTenant(this.getTenantId(), () => {
|
||||
return users.getGlobalUserByEmail(email)
|
||||
})
|
||||
|
@ -264,7 +263,7 @@ class TestConfiguration {
|
|||
}
|
||||
const response = await this._req(user, null, controllers.users.save)
|
||||
const body = response as SaveUserResponse
|
||||
return this.getUser(body.email)
|
||||
return this.getUser(body.email) as Promise<User>
|
||||
}
|
||||
|
||||
// CONFIGS
|
||||
|
|
|
@ -1,17 +1,62 @@
|
|||
import TestConfiguration from "../TestConfiguration"
|
||||
import { TestAPI } from "./base"
|
||||
import {
|
||||
ActivateLicenseKeyRequest,
|
||||
ActivateOfflineLicenseTokenRequest,
|
||||
} from "@budibase/types"
|
||||
|
||||
export class LicenseAPI extends TestAPI {
|
||||
constructor(config: TestConfiguration) {
|
||||
super(config)
|
||||
}
|
||||
|
||||
activate = async (licenseKey: string) => {
|
||||
refresh = async () => {
|
||||
return this.request
|
||||
.post("/api/global/license/activate")
|
||||
.send({ licenseKey: licenseKey })
|
||||
.post("/api/global/license/refresh")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
getUsage = async () => {
|
||||
return this.request
|
||||
.get("/api/global/license/usage")
|
||||
.set(this.config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
}
|
||||
activateLicenseKey = async (body: ActivateLicenseKeyRequest) => {
|
||||
return this.request
|
||||
.post("/api/global/license/key")
|
||||
.send(body)
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
getLicenseKey = async () => {
|
||||
return this.request
|
||||
.get("/api/global/license/key")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
deleteLicenseKey = async () => {
|
||||
return this.request
|
||||
.delete("/api/global/license/key")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
activateOfflineLicense = async (body: ActivateOfflineLicenseTokenRequest) => {
|
||||
return this.request
|
||||
.post("/api/global/license/offline")
|
||||
.send(body)
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
getOfflineLicense = async () => {
|
||||
return this.request
|
||||
.get("/api/global/license/offline")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
deleteOfflineLicense = async () => {
|
||||
return this.request
|
||||
.delete("/api/global/license/offline")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
getOfflineLicenseIdentifier = async () => {
|
||||
return this.request
|
||||
.get("/api/global/license/offline/identifier")
|
||||
.set(this.config.defaultHeaders())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import * as email from "./email"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
|
||||
import * as _pro from "@budibase/pro"
|
||||
const pro = jest.mocked(_pro, true)
|
||||
|
||||
export default {
|
||||
email,
|
||||
pro,
|
||||
...mocks,
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ async function discordResultsNotification(report) {
|
|||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: `**Nightly Tests Status**: ${OUTCOME}`,
|
||||
content: `**Tests Status**: ${OUTCOME}`,
|
||||
embeds: [
|
||||
{
|
||||
title: `Budi QA Bot - ${env}`,
|
||||
|
|
Loading…
Reference in New Issue